Remote Smart Air Quality Monitoring System Using LoRa, SIM800L and  ThingSpeak

Overview

Air pollution is one of the largest environmental and public health challenges in the world today. Air pollution leads to adverse effects on our health, climate and ecosystem. Monitoring air quality is essential to understanding and preventing air pollution, as well as assessing emission sources in order to preserve a healthier air condition and contribute to the fight against the greenhouse effect.

This project aims to create two devices that are able to communicate with each other via LoRa wireless communication. There are many sensors that can be used to monitor air quality, but for this project we’ll focus on two the DSM501A dust sensor and MQ131 ozone gas sensor. These sensors will be connected to the first device with the Ra-02 LoRa module as the client transceiver. The second device will have the LoRa shield as the server transceiver and SIM800L as the gateway connection for ThingSpeak.


Hardware Used

  • Arduino Uno R3 – 2
  • 
    Ra-02 SX1278 LoRa Module – 1
  • 
    Dragino LoRa Shield – 1
  • 
    SIM800L GSM/GPRS Module – 1
  • 
    DSM501A Dust Sensor – 1
  • 
    MQ131 Ozone Gas Sensor – 1
  • 
    Breadboard – 1
  • Jumper Wires


Software Used

Libraries Used


Application Description

Ra-02 SX1278 LoRa Module

The Ra-02 LoRa module can be used for ultra-long distance spread spectrum communication, with compatible FSK (frequency shift keying) remote modulation and demodulation. This provides a solution for the traditional wireless design which has poor distance coverage, anti-interference and power consumption. The Ra-02 can be widely used in a variety of networking occasions such as automatic meter reading, home building automation, security systems, and remote irrigation systems.

Specifications:

  • Interface: SPI
  • Programmable Bit Rate: up to 300 kbps
  • Frequency Range: 410-525 MHz
  • Antenna: IPEX
  • Max Transmit Power: 18 +/- 1 dBm
  • Power Supply: 2.5~3.7 V, Typically 3.3 V
  • Operating Temperature: -30~85 °C

Dragino LoRa Shield

The Dragino LoRa Shield is a long range transceiver on a Arduino shield form factor and based on Open source library. The LoRa Shield allows the user to send data and reach extremely long ranges at low data-rates. It provides ultra-long range spread spectrum communication and high interference immunity whilst minimizing current consumption.

Specifications:

  • Maximum Link Budget: 168 dB
  • Programmable Bit Rate: up to 300 kbps
  • High Sensitivity: down to -148 dBm
  • Bullet-proof front end: IIP3 = -12.5 dBm
  • Rx Current: 10.3 mA
  • Register Retention: 200 nA
  • Synthesizer Resolution: 61 Hz
  • Modulation: FSK, GFSK, MSK, GMSK, LoRaTM and OOK
  • Dynamic Range RSSI: 127 dB

How Do They Work?

They use a technique called spread spectrum communication. Information is transmitted over a much wider bandwidth than is necessary. By doing so, strength against external narrowband interference is increased. Since the wider the bandwidth of any transmitted signal, the lower the relative influence of interference over a small part of the bandwidth will be. The spread spectrum techniques allow for simultaneous multiplexing of multiple transmissions in the same bandwidth. Since multiple users can share the same spread spectrum bandwidth without interfering with one another, these can be called as multiple access techniques.

You can learn more about spread spectrum communications by clicking here.

SIM800L GSM/GPRS Module

The SIM800L is a cellular module which allows for GPRS data transmission, sending and receiving SMS and making and receiving voice calls. The board is compact in size and has low current consumption. It even features a power saving technique which limits current consumption to as low as 1 mA when not in use. To top it off, the module supports quad-band GSM/GPRS network, meaning it works pretty much anywhere in the world.

How does it work?

The module is intended to operate similar to a cellular phone with a SIM card. After it is powered, the module boots up, searches for the nearest cellular network and connects to it automatically. The LED displays on the board indicate the connection state. It blinks fast when there’s no network coverage, but blinks slow when connected.

DSM501A Dust Sensor

The DSM501A dust sensor module is a low cost and compact particle density sensor. It is capable of quantitative particle measurement of fine particles as small as one micron

How does it work?

An infrared emitting diode and a phototransistor are diagonally arranged inside of the device. This allows it to detect the reflected light of dust in the surrounding air. It is especially effective in detecting very fine particles like cigarette smoke, and is commonly used in air purifier systems.

MQ131 Ozone Gas Sensor

The MQ131 Ozone Gas Sensor, as its name suggests, can sense ozone gas (O3). It has a high sensitivity to ozone while also being sensitive to strong oxides such as CL2 and NO2. These are widely used in air quality monitoring applications, whether it is domestic or industrial.

How does it work?

Inside the black bakelite of the ozone gas sensor is a rather sensitive material called Tungsten trioxide (WO3). Its conductivity is high in clean air, but when ozone gas is present in the surrounding, the conductivity gets lower. Users can convert the change of conductivity to correspond to the output signal for the gas concentration detected.


Hardware Setup

Client (Ra-02 LoRa Module with with Sensors)

Ra-02 LoRa Module Connections

  • 3.3V connected to Arduino 3.3V
  • RST connected to Arduino pin 9
  • DI00 connected to Arduino pin 2
  • GND connected to Arduino Ground
  • NSS connected to Arduino pin 10
  • MOSI connected to Arduino pin 11
  • MISO connected to Arduino pin 12
  • SCK connected to Arduino pin 13

DSM501A Dust Sensor Connections

  • 3rd Wire (VCC) connected to Arduino 5V
  • 4th Wire (PM2.5) connected to Arduino pin 7
  • 5th Wire (GND) connected to Arduino Ground

MQ131 Ozone Gas Sensor Connections

  • A0 connected to Arduino pin A0
  • VCC and GND connected to Arduino 5V and Ground respectively

Server (Dragino LoRa Shield with the SIM800L Module)


ThingSpeak Setup

To setup ThingSpeak, first you have to Sign up on their website https://thingspeak.com. If you already have an account, login and click on “Channels.”

In doing so, you should be directed to your channels page. Click on “New Channel” to create a new channel.

Once loaded, you will be presented with the Channel Settings. Fill up whatever is required and check 2 fields, one for each sensor reading display. You only need to give it a name and a description; the rest are unnecessary.

When you’re done, just scroll to the bottom of the page and hit “Save Channel.” After this, you will be able to view the display privately by clicking the “Private View” tab on top. You can customize the field charts by clicking the pencil icon.

You would also need to get the API keys for your Arduino code in order for the SIM800L to transmit the data to ThingSpeak. To check your API key, click on the “API Keys” tab at the top.


Code

Client (Ra-02 LoRa Module with with Sensors)

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>
#include "DSM501.h"
#include "MQ131.h"

#define CLIENT_ADDRESS 123
#define SERVER_ADDRESS 321

// Singleton instance of the radio driver
RH_RF95 driver;
// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(driver, CLIENT_ADDRESS);

int dustSensorPin = 7; //PM2.5
int o3sensorPin = A0;
DSM501 dsm501(6, dustSensorPin);
MQ131 o3sensor(4, o3sensorPin, LOW_CONCENTRATION, 10000);

void setup() {
  Serial.begin(9600);
  if (!manager.init())
    Serial.println("init failed");
  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on
  dsm501.begin(MIN_WIN_SPAN);
  o3sensor.setR0(252564.10); //set base resistance R0 value
  o3sensor.setTimeToRead(30); //set time to read value 30s
}

char buf[RH_RF95_MAX_MESSAGE_LEN];

void loop() {
  dsm501.update();
  Serial.print("Dust Concentration: "); //DSM501A
  Serial.print(dsm501.getParticleWeight(1));
  Serial.println(" µg/m3");

  char data1[8];
  dtostrf(dsm501.getParticleWeight(1), 5, 2, data1);
  // Send a message to manager_server
  if (manager.sendtoWait(data1, sizeof(data1), SERVER_ADDRESS)) {
    // Now wait for a reply from the server
    uint8_t len = sizeof(buf);
    uint8_t from;   
    if (manager.recvfromAckTimeout(buf, &len, &from)) {
      Serial.print("reply from 0x");
      Serial.print(from, HEX);
      Serial.print(": ");
      Serial.println(buf);
    }
    else {
      Serial.println("No reply");
    }
  }
  else {
    Serial.println("sendtoWait failed"); 
  }
  delay(500);

  o3sensor.begin();
  Serial.print("Ozone Gas: ");
  //Serial.print(o3sensor.getO3(PPB)); //safe value is below 50 ppb
  //Serial.print(" ppb\t");
  Serial.print(o3sensor.getO3(UG_M3));
  Serial.println(" µg/m3\t");

  char data2[8];
  dtostrf(o3sensor.getO3(UG_M3), 4, 2, data2);

  if (manager.sendtoWait(data2, sizeof(data2), SERVER_ADDRESS)) {
    // Now wait for a reply from the server
    uint8_t len = sizeof(buf);
    uint8_t from;   
    if (manager.recvfromAckTimeout(buf, &len, &from)) {
      Serial.print("reply from 0x");
      Serial.print(from, HEX);
      Serial.print(": ");
      Serial.println(buf);
    }
    else {
      Serial.println("No reply");
      }
  }
  else {
    Serial.println("sendtoWait failed"); 
  }
}

Server (Dragino LoRa Shield with the SIM800L Module)

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>
#include <SoftwareSerial.h>

SoftwareSerial gsm(2, 3); // RX, TX of Arduino

#define CLIENT_ADDRESS 123
#define SERVER_ADDRESS 321

// Singleton instance of the radio driver
RH_RF95 driver;
// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(driver, SERVER_ADDRESS);

char data[] = "Message Received";
char buf[RH_RF95_MAX_MESSAGE_LEN];
uint8_t index = 1; //1 for first data, 2 for 2nd

void setup() {
  Serial.begin(9600);
  gsm.begin(4800); // Set GSM Baudrate
  if (!manager.init())
    Serial.println("init failed");
  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on
}

void loop() {
  if (manager.available()) {
    if (index == 1) {
      // Wait for a message addressed to us from the client
      uint8_t len = sizeof(buf);
      uint8_t from;
      if (manager.recvfromAck(buf, &len, &from))
      {
        Serial.println(buf);
  
        // Send a reply back to the originator client
        if (!manager.sendtoWait(data, sizeof(data), from))
          Serial.println("sendtoWait failed");
      }
      gsmSend();
      index = 2;
    }
    else if (index == 2) {
      // Wait for a message addressed to us from the client
      uint8_t len = sizeof(buf);
      uint8_t from;
      if (manager.recvfromAck(buf, &len, &from))
      {
        Serial.println(buf);
  
        // Send a reply back to the originator client
        if (!manager.sendtoWait(data, sizeof(data), from))
          Serial.println("sendtoWait failed");
      }
      gsmSend();
      index = 1;
    }
  }
}

void gsmSend() {
  String updateTS = "GET https://api.thingspeak.com/update?api_key=4Z6XNGVB0ZEGKNFJ&field";
  String updateTSvalue = updateTS + index + "=" + atof(buf);
  Serial.println("Sending the data to ThingSpeak");
  gsm.println("AT+CIPMODE=0");
  delay(2000);
  gsm.println("AT+CIPMUX=0");
  delay(2000);
  gsm.println("AT+CGATT=1");
  delay(2000);
  gsm.println("AT+CSTT=\"http.globe.com.ph\"");
  delay(2000);
  gsm.println("AT+CIICR");
  delay(2000);
  gsm.println("AT+CIFSR");
  delay(2000);
  gsm.println("AT+CIPSTART=\"TCP\",\"184.106.153.149\",\"80\"");
  delay(2000);
  gsm.println("AT+CIPSEND=80");
  delay(2000);
  gsm.println(updateTSvalue);   //Send the Serial Monitor Strings(AT Commands) to GSM.
  delay(2000);
  gsm.println("AT+CIPSHUT");
}

Code Breakdown

Client Code

Pre-Initialization

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>
#include "DSM501.h"
#include "MQ131.h"

Before we start, we must include the libraries to be used in the project. The RHReliableDatagram is for sending addressed, acknowledged, and retransmitted datagrams and is under the RH_RF95 library. These two libraries depend on the SPI (Serial Peripheral Interface) to function so that one is included as well. DSM501 and MQ131 libraries are for the dust sensor and ozone gas sensor respectively.

#define CLIENT_ADDRESS 123
#define SERVER_ADDRESS 321

// Singleton instance of the radio driver
RH_RF95 driver;
// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(driver, CLIENT_ADDRESS);

After that, we define the client address and server address. This is important to make sure you’re communicating one-on-one with the your LoRa shield and not any others which could be nearby. Next, we create the RF95 driver object and datagram manager with the driver and client address as the parameters.

int dustSensorPin = 7; //PM2.5
int o3sensorPin = A0;
DSM501 dsm501(6, dustSensorPin);
MQ131 o3sensor(4, o3sensorPin, LOW_CONCENTRATION, 10000);
char buf[RH_RF95_MAX_MESSAGE_LEN];

Next, we assign the pins to the sensors. For the DSM501, the first parameter is for PM1.0 which we assigned to pin 6. The second parameter is for PM2.5. For the MQ131, the first parameter is for the heater which is already integrated into the module so you can assign it to any pin that’s not already used. The ozone gas sensor we are using is the black bakelite low concentration version so we indicate it on the third parameter. The 10000 is the load resistance. Lastly, we initialize the variable buf to store incoming messages.

void setup()

Serial.begin(9600);
  if (!manager.init())
    Serial.println("init failed");
  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on
  dsm501.begin(MIN_WIN_SPAN);
  o3sensor.setR0(252564.10); //set base resistance R0 value
  o3sensor.setTimeToRead(30); //set time to read value 30s

Inside the setup() function, we set the Serial baud rate to 9600 bps which is the default for serial communication. We then put an if statement to check if the reliable datagram manager has initialized or not by printing a message when it fails. After that, we initialize the DSM501 and MQ131. The base resistance of the MQ131 module was obtained by running o3sensor.calibrate() in code and the time to read is set to 30 seconds.

void loop()

dsm501.update();
  Serial.print("Dust Concentration: "); //DSM501A
  Serial.print(dsm501.getParticleWeight(1));
  Serial.println(" µg/m3");

  char data1[8];
  dtostrf(dsm501.getParticleWeight(1), 5, 2, data1);

Inside the loop() function, we grab the dust sensor reading with dsm501.update() and then get the particulate matter density of particles over 2.5 µm using dsm501.getParticleWeight(1). We then have to convert this float value to a string and store it in a variable at the same time using dtostrf().

// Send a message to manager_server
  if (manager.sendtoWait(data1, sizeof(data1), SERVER_ADDRESS)) {
    // Now wait for a reply from the server
    uint8_t len = sizeof(buf);
    uint8_t from;   
    if (manager.recvfromAckTimeout(buf, &amp;len, &amp;from)) {
      Serial.print("reply from 0x");
      Serial.print(from, HEX);
      Serial.print(": ");
      Serial.println(buf);
    }
    else {
      Serial.println("No reply");
    }
  }
  else {
    Serial.println("sendtoWait failed"); 
  }
  delay(500);

This data can then be sent to the server-side using manager.sendtoWait(). This sends the message (with retries) and waits for an acknowledgement. It returns true if an acknowledgement is received which allows it to print the acknowledgement reply using manager.recvfromAckTimeout().

o3sensor.begin();
  Serial.print("Ozone Gas: ");
  //Serial.print(o3sensor.getO3(PPB)); //safe value is below 50 ppb
  //Serial.print(" ppb\t");
  Serial.print(o3sensor.getO3(UG_M3));
  Serial.println(" µg/m3\t");

  char data2[8];
  dtostrf(o3sensor.getO3(UG_M3), 4, 2, data2);

Next, we grab the ozone gas sensor reading with o3sensor.begin() and then get the value reading in µg/m3 using o3sensor.getO3(UG_M3). If you want to get the parts per billion which is more commonly used in global statistical data reports then you can do so with o3sensor.getO3(PBB) instead. Similar to the dust sensor reading above, we then have to convert the float value to a string and store it in a variable at the same time using dtostrf().

if (manager.sendtoWait(data2, sizeof(data2), SERVER_ADDRESS)) {
    // Now wait for a reply from the server
    uint8_t len = sizeof(buf);
    uint8_t from;   
    if (manager.recvfromAckTimeout(buf, &amp;len, &amp;from)) {
      Serial.print("reply from 0x");
      Serial.print(from, HEX);
      Serial.print(": ");
      Serial.println(buf);
    }
    else {
      Serial.println("No reply");
      }
  }
  else {
    Serial.println("sendtoWait failed"); 
  }

This part is similar to the one above where we send the sensor readings to the server-side, but this time it’s for the ozone gas sensor.

Server Code

Pre-Initialization

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>
#include <SoftwareSerial.h>

SoftwareSerial gsm(2, 3); // RX, TX of Arduino

Again, we must include the libraries to be used in the project. The SoftwareSerial library allows serial communication on other digital pins of the Arduino which allows us to use pins 2 and 3 as a receiver and transmitter respectively. This also allows us to use it as a debug serial and print out the message received later on.

#define CLIENT_ADDRESS 123
#define SERVER_ADDRESS 321

// Singleton instance of the radio driver
RH_RF95 driver;
// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(driver, SERVER_ADDRESS);

After that, we define the client address and server address. Again, this is important to make sure you’re communicating one-on-one with the the Ra-02 and not any others which could be nearby so make sure they are the same with what’s in the client-side code. Next, we create the RF95 driver object and datagram manager with the driver and server address as the parameters.

char data[] = "Message Received";
char buf[RH_RF95_MAX_MESSAGE_LEN];
uint8_t index = 1; //1 for first data, 2 for 2nd

Here, we create the message we want to send to the client when the server-side receives a message as well as initialize the variable buf to store the incoming sensor reading data. The index will be used later to distinguish between the two data values received.

void setup()

Serial.begin(9600);
  gsm.begin(4800); // Set GSM Baudrate
  if (!manager.init())
    Serial.println("init failed");
  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on

Inside the setup() function, we set the serial baud rate to 9600 bps which is the default for serial communication and the GSM serial baud rate to 4800 bps. After that, we put an if statement to check if the reliable datagram manager has initialized or not by printing a message when it fails.

SIM800L AT Commands for Sending Data to ThingSpeak

void gsmSend() {
  String updateTS = "GET https://api.thingspeak.com/update?api_key=4Z6XNGVB0ZEGKNFJ&amp;field";
  String updateTSvalue = updateTS + index + "=" + atof(buf);
  Serial.println("Sending the data to ThingSpeak");
  gsm.println("AT+CIPMODE=0");
  delay(2000);
  gsm.println("AT+CIPMUX=0");
  delay(2000);
  gsm.println("AT+CGATT=1");
  delay(2000);
  gsm.println("AT+CSTT=\"http.globe.com.ph\"");
  delay(2000);
  gsm.println("AT+CIICR");
  delay(2000);
  gsm.println("AT+CIFSR");
  delay(2000);
  gsm.println("AT+CIPSTART=\"TCP\",\"184.106.153.149\",\"80\"");
  delay(2000);
  gsm.println("AT+CIPSEND=80");
  delay(2000);
  gsm.println(updateTSvalue);   //Send the Serial Monitor Strings(AT Commands) to GSM.
  delay(2000);
  gsm.println("AT+CIPSHUT");
}

AT (ATtention) commands are instructions used to control a modem. Firstly, AT+CIPMODE=0 sets the TCPIP application mode to normal as this is necessary to govern the connection of the SIM800L to the Internet. Secondly, AT+CIPMUX=0 starts up a single IP connection and AT+CGATT=1 attaches us to the GPRS service. Thirdly, AT+CSTT=”APN” starts task and sets the access point name to the url specified and AT+CIICR brings up the wireless connections with GPRS. You can find the APN to use for the SIM card you have online.

Next, AT+CIFSR gets your local IP address and AT+CIPSTART=”TCP”,”184.106.153.149″,”80″ will start up the TCP connection to ThingSpeak. Lastly, AT+CIPSEND=80 allows you to send data to ThingSpeak with about 80 characters length. This is where the GET command is run. Change the API key in the updateTS string; you have to make sure the API key in the link matches the one in your channel by checking the “API Keys” tab in your ThingSpeak channel. It’s also important to note that AT+CIPSHUT has to be sent by the end of all these to ensure that the GPRS is deactivated. If you don’t shut it down, it may time itself out.

void loop()

if (manager.available()) {
    if (index == 1) {
      // Wait for a message addressed to us from the client
      uint8_t len = sizeof(buf);
      uint8_t from;
      if (manager.recvfromAck(buf, &amp;len, &amp;from))
      {
        Serial.println(buf);
  
        // Send a reply back to the originator client
        if (!manager.sendtoWait(data, sizeof(data), from))
          Serial.println("sendtoWait failed");
      }
      gsmSend();
      index = 2;
    }

Inside the loop() function, we check for when there’s a message received with manager.available() and if index is 1, that means it’s the first sensor’s data. recvfromAck() will get the data from the buffer and print it in the serial monitor. Once done, it will call the gsmSend() function which contains all the AT commands to send the data to ThingSpeak then set the index to 2 so it knows the second sensor’s data is next to come.

else if (index == 2) {
      // Wait for a message addressed to us from the client
      uint8_t len = sizeof(buf);
      uint8_t from;
      if (manager.recvfromAck(buf, &amp;len, &amp;from))
      {
        Serial.println(buf);
  
        // Send a reply back to the originator client
        if (!manager.sendtoWait(data, sizeof(data), from))
          Serial.println("sendtoWait failed");
      }
      gsmSend();
      index = 1;
    }

This part is similar to the one above where we send the data to ThingSpeak, but this time it’s for the second received data which is from the second sensor.


Conclusion

LoRa is a low data rate, low power, and low power wireless platform technology for building IoT networks. It is more cost efficient in the long run compared to other alternatives due to its wide list of features. The transceivers can communicate in up to 15 kilometers distance away from each other which is really far, often more than enough for many applications. LoRa Technology operates in the unlicensed ISM band, therefore is legal and great for educational purposes.

The sensors I used for this project were a bit harder to test due to the difficulty of finding values to compare. One has to be very specific in searching online for air quality monitoring value standards for dust particulate matter and ozone gas concentration. Luckily, the libraries we have for both sensors handle the calculation for us.


Sensor Reading Comparison

These are the guidelines or standards defined by the World Health Organization for dust particulate matter PM2.5 and Ground-Level Ozone Gas:

To get the AQI (Air Quality Index) for comparison, you can use an online AQI calculator.


References

The post Remote Smart Air Quality Monitoring System Using LoRa, SIM800L and ThingSpeak appeared first on CreateLabz.

Air quality monitoringArduinoDragino lora shieldDsm501Dsm501aDust sensorGprsGsmKnowledgebaseLoraMq131Ozone gas sensorRa-02RemoteRf95RhreliabledatagramRh_rf95Sim800lSpread spectrum communicationSx1278Thingspeak

Leave a comment

All comments are moderated before being published