Configurable Indoor Air Quality Monitoring System

1. Introduction

The aim of this project is to create the infrastructure for a configurable indoor air quality monitoring system. The principle behind this idea is to have multiple devices that can be placed anywhere in an enclosed area. Depending on the user’s needs, the devices can be reconfigured accordingly to meet any demand, without reprogramming them.

Each device is separated into 2 boards, the first one which has multiple sensors for monitoring the environment, and the second one which’s purpose is to acquire the data read by the previous board and send it to a server. The communication between the two boards will be done via a serial protocol. Being small and maneuverable, the device can be easily placed in different parts of the room, and thus, it must be powered up by a battery. The data acquired will be monitored using a custom web page.

Project structure:

2. Hardware description

Depending on their functionality, the boards can be categorized in:

  • Data transmitting board ( later referred as the master board )
  • Data acquiring board ( later referred as the slave board )

Each board has its own MCU ( Microcontroller Unit ). To demonstrate the functionality of the project, 3 boards will be developed ( 1 master and 2 slave boards ), and their main components of the devices are listed below:

  1. Master board
  2. Slave board 1
    1. 1x Arduino Nano board ( with ATMEGA328P MCU )
    2. 1x ADT7410 module ( high accuracy digital temperature sensor )
    3. 1x MQ2 module( analog gas sensor )
  3. Slave board 2
    1. 1x Microphone module ( sound sensor )
    2. 1x DS18B20 module ( digital temperature sensor )

3. Hardware implementation

3.1 Schematic and layouts

The schematics of the master and slave boards and the layout of the master board were developed using Altium Designer, which offers one of the most advanced tools to develop any electronics project.

3.1.1 ESP32 master board

The master board is powered up by a battery pack ( with two 18650 cells in series ). A diode was placed to protect the circuit against reversed input polarity. The power can be turned on or off using a rocker switch, which is placed outside the case. The nominal voltage of an 18650 cell is approximately 3.7 volts, but it depends on the current state of charge of the battery. Since 2 of them are used in series, the input voltage of the board will be ~7.4 volts. This board must provide 3V3 and 5V for the ESP32 and the slave board. Having both 3V3 and 5V gives a lot of possible sensor combinations to satisfy any requirements.

Since the application is battery powered, any linear voltage regulator will be unsuited for this application ( to convert from 7V4 to 3V3 or 5V ), because a lot of power will be lost during the conversion. Also, the amount of drawn current by the slave board is unknown, so two MCP16311 regulators were used. MCP16311 is an Integrated Synchronous Switch Step-Down Regulator, with 1 A max current output. The main advantage of this chip is the efficiency during lighter and heavier loads. The switching frequency is 500 kHz, and low and high side switches are integrated into the capsule.

The ESP32 board can be connected to the master board using two female headers. The input voltage can be measured using a voltage divider tied to the input. The J1 connector is used to power and communicate with the slave board.

The master board schematic:

The layout of the master board was optimized to have minimum size and to be manufactured as a single sided board, which means that the number of bridges on the other side must be as low as it can. This board can be mounted using 3 M3x8 screws. 3 optional LEDs can be used for debugging purposes.

The master board layout ( top view ):

The master board 3D model ( top view ):

The master board 3D model ( bottom view ):

The final master board after soldering ( top view ):

The final master board after soldering ( bottom view ):

3.1.2 ATMEGA328P slave boards

The Arduino Nano is powered with 5V, so its digital voltage level is 5V. Since the ESP32 digital voltage level is at 3V3, a level shifter between the 5V and 3V3 was used to prevent damaging the ESP32 pins.

The ADT7410 sensor uses I2C to communicate, and it can generate 2 interrupts. The I2C address can be set by either pulling the A0 and/or A1 to GND or VCC. In this case, both pins were tied to GND, which will set the I2C address of the sensor to 0x48. The output of the MQ2 sensor is an analog value, and it can react to several gases, such as Propane, Hydrogen, Methane etc.

The slave 1 board schematic:

The output of the sound sensor is an analog value, and the DS18B20 sensor provides the temperature reading using the 1-wire protocol.

The slave 2 board schematic:

The slave boards were developed using 2 perforated printed boards, and the connections were realized by hand.

Slave 1 board after soldering ( top view ):

Slave 1 board after soldering ( bottom view ):

Slave 2 board after soldering ( top view ):

Slave 2 board after soldering ( bottom view ):

3.2 3D model case

The case was designed using Fusion 360 3D modeling software, and sliced for 3D printing using Prusa Slicer.

The boards are placed in a case, which is divided into 2 parts: one for the battery, rocker switch, and the master board and the other one is used for the slave board. The case is composed by 3 pieces, as presented below:

  • Middle part → the slave and master boards are mounted on this part using 7 M3 screws, and they are connected by a cable. A 3 mm distance on both sides is left for spacing between the middle part and boards; to prevent the use of supports which would waste time and material, the spacing on one side will be realized using separate components ( front view, left side ).

Middle part ( front view ):

Middle part ( left side ):

  • Bottom part → the compartments can be observed in these images; the middle part is place inside using the guide rail in the center of the bottom part; the rocker switch is fixed on the left side of the part, and on the right side the battery pack is placed; 4 threaded heat inserts are used for the mounting screws;

Bottom part ( left view ):

Bottom part ( right view ):

  • Top part → this part will close the case and keep it in place using 4 M3x10 screws; the extrusion on the right side will hold the battery pack in place

Top part ( top view ):

Top part ( bottom view ):

3.3 Final product

The final product can be seen in the images below ( left and top view ):

Final product ( left view ):

Final product ( top view ):

4. Software implementation

4.1 MCU code

The serial communication between the master and slave boards is implemented using the UART protocol, which runs at the 115200 baud rate, 8 bits, no parity. Any exchange of data between those two is started by the master board. At startup, the master will request the supported parameter list which can be monitored by the slave boards, and it will read and process the sensor data. The master will stop working if a timeout occurs ( the slave does not respond in 5 seconds ).

The communication state machine can be seen in the image below:

Two commands are implemented, UART_CMD_READ_SENSORS_STATUS will read the configuration at startup, and the UART_CMD_READ_SENSORS_DATA will request the data from the sensors. The length of both commands is 2 bytes, the responses for the first and second one are 4 and 8, respectively.

4.1.1 ESP32 code

The WiFi SSID and password, and the MQTT server IP are required to establish a connection to the MQTT broker

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// WIFI SETUP
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
 
const char* ssid        = "TO_BE_REPLACED"; // WiFi SSID
const char* password    = "TO_BE_REPLACED"; // WiFi password
const char* mqtt_server = "TO_BE_REPLACED"; // MQTT server
 
WiFiClient   espClient;
PubSubClient client( espClient );

The functions used for WiFi:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// WIFI FUNCTIONS
// This functions connects your ESP8266 to your router
void setup_wifi() {
    delay( 10 );
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println( ssid );
    Serial.print("Password used: ");
    Serial.println( password );
    WiFi.mode( WIFI_STA );
    WiFi.begin( ssid, password );
    while( WiFi.status() != WL_CONNECTED ) 
    {
        delay( 500 );
        Serial.print( "." );
    }
    Serial.println("");
    Serial.print( "WiFi connected - ESP IP address: " );
    Serial.println( WiFi.localIP() );
}
 
// This function is executed when some device publishes a message to a topic that your ESP8266 is subscribed to
// Change the function below to add logic to your program, so when a device publishes a message to a topic that 
// your ESP8266 is subscribed you can actually do something
void callback( String topic, byte* message, unsigned int length ) 
{
    Serial.print( "Message arrived on topic: " );
    Serial.println( topic );
}
 
// This functions reconnects your ESP8266 to your MQTT broker
// Change the function below if you want to subscribe to more topics with your ESP8266 
void reconnect() 
{
    while( !client.connected() ) 
    {
        Serial.print( "Attempting MQTT connection..." );
        if( client.connect( "ESP8266Client" ) )
        {
            Serial.println( "connected" );  
        } 
        else 
        {
            Serial.print( "failed, rc=" );
            Serial.print( client.state() );
            Serial.println( " try again in 5 seconds" );
            delay( 5000 );
        }
    }
}

The serial communication can be configured by modifying the macros below:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SERIAL COMM SETUP
// general macros
#define TRUE                                        true
#define FALSE                                       false
 
#define SERIAL_1_BAUDRATE                           115200
#define SERIAL_2_BAUDRATE                           115200
 
#define ESP_MASTER_RX_PIN                           15
#define ESP_MASTER_TX_PIN                           2
 
#define UART_CHECK_COUNTER_MAX                      10
#define UART_TIMEOUT_COUNTER_MAX                    50
 
#define UART_END_BYTE				    0x55
 
// command list
#define UART_CMD_READ_SENSORS_STATUS		    0x10
#define UART_CMD_READ_SENSORS_DATA		    0x20
 
// flags pos
#define SENSOR_TEMP_SUPPORTED                       0x01
#define SENSOR_TVOC_SUPPORTED                       0x02
#define SENSOR_SOUND_SUPPORTED                      0x04
 
#define UART_REQUEST_CMD_MAX_LENGTH	            2
#define UART_RESPONSE_READ_DATA_MAX_LENGTH	    8
#define UART_RESPONSE_INIT_CMD_MAX_LENGTH	    4
 
#define UART_READ_BUFFER_LENGTH		            10
#define UART_WRITE_BUFFER_LENGTH		    5
 
// sensor flag position
#define SENSOR_TEMP_SUPPORTED                       0x01
#define SENSOR_TVOC_SUPPORTED                       0x02
#define SENSOR_SOUND_SUPPORTED                      0x04
 
// UART flags structure
typedef union
{   
    struct{
        uint8_t is_busy                 :1,
                sensor_temp_supported   :1,
                sensor_tvoc_supported   :1,
                sensor_sound_supported  :1,
                                        :4;
    };
 
    uint8_t word8;
}UART_COM_FLAGS;
 
// UART main structure
typedef struct
{   
	uint8_t write_buffer[UART_WRITE_BUFFER_LENGTH];
	uint8_t read_buffer[UART_READ_BUFFER_LENGTH];
	uint8_t buffer_counter;
 
    uint8_t check_counter;
    uint8_t timeout_counter;
 
    UART_COM_FLAGS flags;
}UART_COM_STRUCT;
 
UART_COM_STRUCT UART_com;

The starting configuration of the serial protocol is described below. When the function is called, it will write the starting command UART_CMD_READ_SENSORS_STATUS to read the slave capabilities. If no response is received, the master board will stop working.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SETUP FUNCTION
void init_com_protocol_UART()
{
    UART_com.flags.word8     = 0x00;
    UART_com.flags.is_busy   = TRUE;
    UART_com.timeout_counter = UART_TIMEOUT_COUNTER_MAX;
    UART_com.check_counter   = UART_CHECK_COUNTER_MAX;
    UART_com.buffer_counter  = 0;
    UART_com.write_buffer[0] = UART_CMD_READ_SENSORS_STATUS;
    UART_com.write_buffer[1] = UART_END_BYTE;
    Serial2.write( UART_com.write_buffer, 2 );
}

The WiFi and the serial communications are initialized in the setup functions

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SETUP FUNCTION
void setup() {  
    // setup the serial comms
    Serial.begin(  SERIAL_1_BAUDRATE );
    Serial2.begin( SERIAL_2_BAUDRATE, SERIAL_8N1, ESP_MASTER_RX_PIN, ESP_MASTER_TX_PIN );
    delay( 4000 );
 
    // prepare wifi config
    setup_wifi();
    client.setServer( mqtt_server, 1883 );
    client.setCallback( callback );
 
    // init the serial config
    init_com_protocol_UART();
 
    Serial.println( "data sent" );
}

The master will send periodically the UART_CMD_READ_SENSORS_DATA command, and after the response is received, it will send the data to the MQTT broker, depending on the slave's configuration.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// LOOP FUNCTION
void loop() 
{ 
    uint8_t data_byte_read;
    int8_t  flags_processed;
 
    // reconnect to MQTT
    if( !client.connected() ) 
    {
        reconnect();
    }
    if( !client.loop() )
    {
        client.connect( "ESP8266Client" );
    }
 
    // check for timeouts
    if( UART_com.flags.is_busy == TRUE )
    {
        UART_com.timeout_counter--;
 
        if( UART_com.timeout_counter == 0 )
        {
            UART_com.flags.word8 = 0x00;
            Serial.println( "Communication has been reset and blocked!" );
            while( 1 );
        }
    }
    else
    {
        UART_com.check_counter--;
 
        // read the sensors again
        if( UART_com.check_counter == 0 )
        {
            UART_com.flags.is_busy   = TRUE;
            UART_com.check_counter   = UART_CHECK_COUNTER_MAX;
            UART_com.timeout_counter = UART_TIMEOUT_COUNTER_MAX;
 
            UART_com.write_buffer[0] = UART_CMD_READ_SENSORS_DATA;
            UART_com.write_buffer[1] = UART_END_BYTE;
            Serial2.write( UART_com.write_buffer, 2 );
        }
 
    }
 
    // read every byte available from the receiver's buffer
    while( Serial2.available() )
    {
        data_byte_read = Serial2.read();
 
        if( UART_com.flags.is_busy == TRUE )
        {
            UART_com.timeout_counter = UART_TIMEOUT_COUNTER_MAX;
            UART_com.check_counter   = UART_CHECK_COUNTER_MAX; 
            UART_com.read_buffer[UART_com.buffer_counter] = data_byte_read;
            UART_com.buffer_counter++;
 
            if( ( ( UART_com.buffer_counter == UART_RESPONSE_INIT_CMD_MAX_LENGTH  ) && ( UART_com.write_buffer[0] == UART_CMD_READ_SENSORS_STATUS ) ) ||
                ( ( UART_com.buffer_counter == UART_RESPONSE_READ_DATA_MAX_LENGTH ) && ( UART_com.write_buffer[0] == UART_CMD_READ_SENSORS_DATA   ) ) )
            {
                UART_com.flags.is_busy = FALSE;
                UART_com.buffer_counter = 0;
 
                switch( UART_com.read_buffer[0] )
                {
                    case UART_CMD_READ_SENSORS_STATUS:
                        Serial.println( "Config read ..." );
 
                        flags_processed = ( int8_t )( ( ( ( uint16_t )UART_com.read_buffer[2] ) << 8 ) | UART_com.read_buffer[1] );
 
                        UART_com.flags.sensor_temp_supported  = ( ( flags_processed & SENSOR_TEMP_SUPPORTED  ) != 0x00 ) ? TRUE : FALSE;
                        UART_com.flags.sensor_tvoc_supported  = ( ( flags_processed & SENSOR_TVOC_SUPPORTED  ) != 0x00 ) ? TRUE : FALSE;
                        UART_com.flags.sensor_sound_supported = ( ( flags_processed & SENSOR_SOUND_SUPPORTED ) != 0x00 ) ? TRUE : FALSE;
                        break;
 
                    case UART_CMD_READ_SENSORS_DATA:
                        Serial.println( "Data read ..." );
 
                        if( UART_com.flags.sensor_temp_supported  == TRUE ) { client.publish("ESP32/temp",  String( ( float )( ( ( uint16_t )UART_com.read_buffer[2] << 8 ) | UART_com.read_buffer[1] ) / 10 ).c_str() ); }
                        if( UART_com.flags.sensor_tvoc_supported  == TRUE ) { client.publish("ESP32/tvoc",  String( ( int16_t )( ( ( uint16_t )UART_com.read_buffer[4] << 8 ) | UART_com.read_buffer[3] ) ).c_str() ); }
                        if( UART_com.flags.sensor_sound_supported == TRUE ) { client.publish("ESP32/sound", String( ( int16_t )( ( ( uint16_t )UART_com.read_buffer[6] << 8 ) | UART_com.read_buffer[5] ) ).c_str() ); }
 
                        break;
                }
            }
        }
    }
 
    delay( 100 );
}

4.1.2 ATMEGA328P code

The same code is used for both slave 1 and 2. To switch between them, the USE_SLAVE_1 or USE_SLAVE_2 must be defined. They cannot be both defined or not defined at the same time.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SETUP SLAVE TO BUILD
#define USE_SLAVE_1
#undef  USE_SLAVE_1
#define USE_SLAVE_2
// #undef  USE_SLAVE_2
 
 
// checking the config 
#if defined( USE_SLAVE_1 ) && defined( USE_SLAVE_2 )
    #error Multiple slaves defined at the same time!
#endif
#if !defined( USE_SLAVE_1 ) && !defined( USE_SLAVE_2 )
    #error No slaves defined!
#endif

Depending on which board is defined, a specific set of sensors will be used.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SLAVE 1 CONFIG
#ifdef USE_SLAVE_1
    #define USE_SENSOR_TEMP_ADT7410
    #define USE_SENSOR_TVOC_MQ2
#endif
 
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SLAVE 2 CONFIG
#ifdef USE_SLAVE_2
    #define USE_SENSOR_SOUND_LM393
    #define USE_SENSOR_TEMP_DS18B20
#endif

The configurations and libraries that are needed by the sensors are defined depending on the previous macros. The following functions are used to read the data provided by them.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SENSOR FUNCTIONS
#ifdef USE_SENSOR_TVOC_MQ2
    #define SENSOR_TVOC_MQ2_PIN             A7
 
    int16_t measure_sensor_tvoc_MQ2() 
    {
        return ( int16_t )( analogRead( SENSOR_TVOC_MQ2_PIN ) );
    }
#endif
 
#ifdef USE_SENSOR_TEMP_DS18B20
    #include <OneWire.h>
    #include <DallasTemperature.h>
 
    #define SENSOR_TEMP_DS18B20_PIN         7
 
    OneWire oneWire( SENSOR_TEMP_DS18B20_PIN );
    DallasTemperature sensor_DS18B20( &oneWire );
 
    int16_t measure_sensor_temp_DS18B20() 
    {
        sensor_DS18B20.requestTemperatures();
        return ( int16_t )( sensor_DS18B20.getTempCByIndex( 0 ) * 10 );
    }
#endif
 
#ifdef USE_SENSOR_SOUND_LM393
    #define SENSOR_SOUND_LM393_PIN          A6
 
    int16_t measure_sensor_SOUND_LM393() 
    {
        return ( int16_t )( analogRead( SENSOR_SOUND_LM393_PIN ) );
    }
#endif
 
#ifdef USE_SENSOR_TEMP_ADT7410
 
    #include <Wire.h>
    #include <Adafruit_Sensor.h>
    #include "Adafruit_ADT7410.h"
 
    Adafruit_ADT7410 temp_sensor_ADT7410 = Adafruit_ADT7410();
 
    float measure_sensor_temp_ADT7410() 
    {
        return ( int16_t )( temp_sensor_ADT7410.readTempC() * 10 );
    }
#endif

The function below is used to initialize all sensors that are being used.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// INIT ALL SENSOR CONFIG
void init_all_sensor_config()
{
#ifdef USE_SENSOR_TVOC_MQ2
    pinMode( SENSOR_TVOC_MQ2_PIN, INPUT );
#endif
#ifdef USE_SENSOR_TEMP_DS18B20
    pinMode( SENSOR_TEMP_DS18B20_PIN, INPUT );
#endif
#ifdef USE_SENSOR_SOUND_LM393
    pinMode( SENSOR_SOUND_LM393_PIN, INPUT );
#endif
#ifdef USE_SENSOR_TEMP_ADT7410
    if( !temp_sensor_ADT7410.begin() )
    {
        Serial.println( "Couldn't find ADT7410!" );
        // while( 1 );
    }
#endif
}

The serial communication is implemented using the following macros:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SERIAL MACROS AND VARIABLES
// general macros used
#define UART_REQUEST_CMD_MAX_LENGTH				        2
#define UART_RESPONSE_CMD_MAX_LENGTH			                4
#define UART_CMD_BYTE_POS						0
#define UART_END_BYTE_POS						1
#define UART_END_BYTE							0x55
 
// commands used
#define UART_CMD_READ_SENSORS_STATUS			                0x10
#define UART_CMD_READ_SENSORS_DATA			                0x20
 
// flag positions
#define SENSOR_TEMP_SUPPORTED                  	                        0x01
#define SENSOR_TVOC_SUPPORTED                  	                        0x02
#define SENSOR_SOUND_SUPPORTED                 	                        0x04
 
// length of buffers
#define UART_READ_BUFFER_LENGTH			                        5
#define UART_WRITE_BUFFER_LENGTH		                        5
 
typedef struct
{
	uint8_t write_buffer[UART_WRITE_BUFFER_LENGTH];
	uint8_t read_buffer[UART_READ_BUFFER_LENGTH];
 
	uint8_t buffer_counter;
}SERIAL_COM;
 
SERIAL_COM serial_com;

The UART protocol is started with the 115200 baud rate and the sensors are initialized at startup.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// SETUP FUNCTION
void setup() 
{
    Serial.begin( 115200 );
    delay( 1000 );
 
    init_all_sensor_config();
}

The commands are processed as they are received. The data from the sensors is read as it is requested.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// LOOP FUNCTION
void loop() 
{
    int16_t response_data_word16;
 
	if( Serial.available() == true )		
	{
		serial_com.read_buffer[serial_com.buffer_counter] = ( uint8_t )Serial.read();
		serial_com.buffer_counter++;
 
		if( serial_com.buffer_counter == UART_REQUEST_CMD_MAX_LENGTH )
		{
			serial_com.buffer_counter = 0;			
			response_data_word16 	  = -32768;
 
                        serial_com.write_buffer[0] = serial_com.read_buffer[0];
			switch( serial_com.read_buffer[UART_CMD_BYTE_POS] )
			{
 
				case UART_CMD_READ_SENSORS_STATUS:
					response_data_word16 = ( int16_t )( 0x00 
							#if defined( USE_SENSOR_TEMP_ADT7410 ) || defined( USE_SENSOR_TEMP_DS18B20 )
								| ( SENSOR_TEMP_SUPPORTED )
							#endif
							#if defined( USE_SENSOR_TVOC_MQ2 )
								| ( SENSOR_TVOC_SUPPORTED )
							#endif
							#if defined( USE_SENSOR_SOUND_LM393 )
								| ( SENSOR_SOUND_SUPPORTED )
							#endif
                                                        );                            
 
                                        serial_com.write_buffer[1] = response_data_word16          & 0xFF;			
                                        serial_com.write_buffer[2] = ( response_data_word16 >> 8 ) & 0xFF;	
                                        serial_com.write_buffer[3] = UART_END_BYTE;
            			        Serial.write( serial_com.write_buffer, 4 );
					break;
 
				case UART_CMD_READ_SENSORS_DATA:
					#ifdef USE_SENSOR_TEMP_ADT7410
						response_data_word16 = measure_sensor_temp_ADT7410();
					#else
						#ifdef USE_SENSOR_TEMP_DS18B20
							response_data_word16 = measure_sensor_temp_DS18B20();
                                                #else
                                                        response_data_word16 = 0x00;
                                                #endif
					#endif
                                        serial_com.write_buffer[1] = response_data_word16 	   & 0xFF;			
                                        serial_com.write_buffer[2] = ( response_data_word16 >> 8 ) & 0xFF;	
 
					#ifdef USE_SENSOR_TVOC_MQ2
					    response_data_word16 = measure_sensor_tvoc_MQ2();
                                        #else
                                            response_data_word16 = 0x00;
					#endif
 
                                        serial_com.write_buffer[3] = response_data_word16          & 0xFF;			
                                        serial_com.write_buffer[4] = ( response_data_word16 >> 8 ) & 0xFF;	
 
					#ifdef USE_SENSOR_SOUND_LM393
                				response_data_word16 = measure_sensor_SOUND_LM393();
                                        #else
                                                response_data_word16 = 0x00;
					#endif
 
                                        serial_com.write_buffer[5] = response_data_word16          & 0xFF;			
                                        serial_com.write_buffer[6] = ( response_data_word16 >> 8 ) & 0xFF;	
 
                                        serial_com.write_buffer[7] = UART_END_BYTE;
		               	        Serial.write( serial_com.write_buffer, 8 );
					break;
			}
		}
	}
}

4.2 Node-RED

The data acquired by the master board and send to a server can be view using the following Dashboard:

The Dashboard was developed using Node-RED, which is a programming tool used for creating web pages. It provides a browser-based editor from which we can wire together flows between different components.

MQTT protocol and TCP/IP were used to establish the communication between the ESP32 and Dashboard. The components of the MQTT protocol are the following:

  • MQTT client → multiple clients can connect to network; in our case there are 2 clients, the website and the master board
  • MQTT broker → it handles the communication between the MQTT clients; the protocol cannot function without it; the broker is hosted locally on laptop

MQTT architecture:

The MQTT is a lightweight protocol designed for IOT devices, since the clients do not need to connect or keep tracking other clients, they only need to exchange data with the broker, using topics. A client can publish ( post data ) or subscribe ( receive data ) on a topic. In our case, 3 topics were implemented for each type of data ( temperature, TVOC and sound ).

The Node-RED flow code:

[ { "id": "156fbaa406ad8123", "type": "tab", "label": "Flow 1", "disabled": false, "info": "", "env": [] }, { "id": "203987ec71ee9f87", "type": "mqtt in", "z": "156fbaa406ad8123", "name": "ESP32/temp", "topic": "ESP32/temp", "qos": "2", "datatype": "auto-detect", "broker": "a03b8463c0d7b983", "nl": false, "rap": true, "rh": 0, "inputs": 0, "x": 370, "y": 300, "wires": [ [ "9a7d31c60c80b677", "3804aaebb1665203" ] ] }, { "id": "a187927df009e949", "type": "ui_gauge", "z": "156fbaa406ad8123", "name": "TVOC", "group": "f6d1c6bb403af6e6", "order": 0, "width": 0, "height": 0, "gtype": "wave", "title": "TVOC gauge", "label": "units", "format": "{{value}}", "min": 0, "max": "1023", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 530, "y": 380, "wires": [] }, { "id": "830a1915f1ec3c49", "type": "mqtt in", "z": "156fbaa406ad8123", "name": "ESP32/tvoc", "topic": "ESP32/tvoc", "qos": "2", "datatype": "auto-detect", "broker": "a03b8463c0d7b983", "nl": false, "rap": true, "rh": 0, "inputs": 0, "x": 370, "y": 380, "wires": [ [ "a187927df009e949", "2759dc64eb94826a" ] ] }, { "id": "fb719ceab83b097f", "type": "mqtt in", "z": "156fbaa406ad8123", "name": "ESP32/sound", "topic": "ESP32/sound", "qos": "2", "datatype": "auto-detect", "broker": "a03b8463c0d7b983", "nl": false, "rap": true, "rh": 0, "inputs": 0, "x": 370, "y": 460, "wires": [ [ "7c6e3bf5c472d3e7", "3bb8702e06e15257" ] ] }, { "id": "9a7d31c60c80b677", "type": "ui_chart", "z": "156fbaa406ad8123", "name": "Temperature", "group": "f456dba3c69cf4bc", "order": 1, "width": 0, "height": 0, "label": "Temperature graph", "chartType": "line", "legend": "false", "xformat": "HH:mm:ss", "interpolate": "linear", "nodata": "", "dot": false, "ymin": "", "ymax": "", "removeOlder": 1, "removeOlderPoints": "", "removeOlderUnit": "3600", "cutout": 0, "useOneColor": false, "useUTC": false, "colors": [ "#1f77b4", "#aec7e8", "#ff7f0e", "#2ca02c", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5" ], "outputs": 1, "useDifferentColor": false, "className": "", "x": 550, "y": 300, "wires": [ [] ] }, { "id": "7c6e3bf5c472d3e7", "type": "ui_chart", "z": "156fbaa406ad8123", "name": "Sound", "group": "fa9c6e33a42140ac", "order": 1, "width": 0, "height": 0, "label": "Sound chart", "chartType": "line", "legend": "false", "xformat": "HH:mm:ss", "interpolate": "linear", "nodata": "", "dot": false, "ymin": "", "ymax": "", "removeOlder": 1, "removeOlderPoints": "", "removeOlderUnit": "3600", "cutout": 0, "useOneColor": false, "useUTC": false, "colors": [ "#1f77b4", "#aec7e8", "#ff7f0e", "#2ca02c", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5" ], "outputs": 1, "useDifferentColor": false, "className": "", "x": 530, "y": 460, "wires": [ [] ] }, { "id": "3bb8702e06e15257", "type": "debug", "z": "156fbaa406ad8123", "name": "debug 1", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 540, "y": 500, "wires": [] }, { "id": "3804aaebb1665203", "type": "debug", "z": "156fbaa406ad8123", "name": "debug 2", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 540, "y": 340, "wires": [] }, { "id": "2759dc64eb94826a", "type": "debug", "z": "156fbaa406ad8123", "name": "debug 3", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 540, "y": 420, "wires": [] }, { "id": "a03b8463c0d7b983", "type": "mqtt-broker", "name": "ESP32", "broker": "localhost", "port": "1883", "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": "4", "keepalive": "60", "cleansession": true, "autoUnsubscribe": true, "birthTopic": "ESP32", "birthQos": "1", "birthRetain": "false", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closeQos": "0", "closeRetain": "false", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willRetain": "false", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" }, { "id": "f6d1c6bb403af6e6", "type": "ui_group", "name": "TVOC", "tab": "8593b1ea584e9609", "order": 2, "disp": true, "width": "6", "collapse": false, "className": "" }, { "id": "f456dba3c69cf4bc", "type": "ui_group", "name": "Temperature", "tab": "8593b1ea584e9609", "order": 1, "disp": true, "width": "6", "collapse": false, "className": "" }, { "id": "fa9c6e33a42140ac", "type": "ui_group", "name": "Sound", "tab": "8593b1ea584e9609", "order": 3, "disp": true, "width": "6", "collapse": false, "className": "" }, { "id": "8593b1ea584e9609", "type": "ui_tab", "name": "Home", "icon": "dashboard", "order": 1, "disabled": false, "hidden": false } ]

5. Conclusion

In conclusion, this project’s aim was to create the infrastructure for a configurable indoor air quality monitoring system, which was successfully obtained. The advantages of using this infrastructure are that any device can be added with ease to the Dashboard, and the MQTT can support a large number ( even reach up to 1000 clients ), and the configuration of a device can be changed, requiring a minimum amount of effort.

Resources

iothings/proiecte/2023/configurableindoorairqualitymonitoringsystem.txt · Last modified: 2024/01/16 02:37 by andrei.enescu0512
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