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

 

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

01 Overview

Air pollution is one of the largest environmental and public health challenges in the world today. It 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 healthier air conditions and contribute to the fight against the greenhouse effect.

This project creates two devices that communicate with each other via LoRa wireless communication. There are many sensors that can be used to monitor air quality, but this project focuses on two: the DSM501A dust sensor and the MQ131 ozone gas sensor. These sensors are connected to the first device, which uses the Ra-02 LoRa module as the client transceiver. The second device uses a LoRa shield as the server transceiver and a SIM800L as the gateway connection to ThingSpeak.

Project Use Case

This project suits makers, students, or environmental monitoring enthusiasts who want to build a long-range, cellular-connected air quality station — placing sensors somewhere remote (no Wi-Fi needed) and viewing the readings on a ThingSpeak dashboard from anywhere.

02 Hardware and Software Components

Gather everything below before you start.

Hardware Components

Component Description
Arduino Uno R3 One board acts as the client (sensor node); the other acts as the server (gateway node).
Ra-02 SX1278 LoRa Module Client-side LoRa transceiver that transmits sensor readings.
Dragino LoRa Shield Server-side LoRa transceiver in Arduino shield form factor.
SIM800L GSM/GPRS Module Gateway connection that sends received sensor data to ThingSpeak over cellular network.
DSM501A Dust Sensor Measures fine particulate matter (dust) concentration.
MQ131 Ozone Gas Sensor Measures ozone (O3) gas concentration.
Breadboard Platform for building and testing the client-side circuit.
Jumper Wires Connects the LoRa module and sensors to the Arduino and breadboard.

Software Tools

Software Description
Arduino IDE Used to write, compile, and upload the client and server sketches.
ThingSpeak Cloud platform used to log and visualize the sensor data sent by the SIM800L.

Libraries Used

Library Purpose
SPI (Serial Peripheral Interface) Required by the RadioHead libraries to communicate with the LoRa radio.
RHReliableDatagram Handles addressed, acknowledged, and retransmitted LoRa messages.
RH_RF95 Low-level driver for the RF95/SX127x-family LoRa radio.
DSM501 Driver for the DSM501A dust sensor.
MQ131 Driver for the MQ131 ozone gas sensor.
SoftwareSerial Enables serial communication with the SIM800L on non-default Arduino pins.

03 Application Discussion

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 traditional wireless designs that have poor distance coverage, anti-interference, and power consumption. The Ra-02 can be widely used in networking scenarios such as automatic meter reading, home/building automation, security systems, and remote irrigation systems.

Specifications

Spec Value
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.7V, typically 3.3V
Operating Temperature -30 to 85°C

Dragino LoRa Shield

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

Specifications

Spec Value
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, LoRa™, OOK
Dynamic Range RSSI 127 dB

How Do They Work?

LoRa modules use a technique called spread spectrum communication. Information is transmitted over a much wider bandwidth than is strictly necessary, which increases resistance to external narrowband interference — the wider the bandwidth of a transmitted signal, the lower the relative influence of interference over any small part of that bandwidth. Spread spectrum techniques also allow simultaneous multiplexing of multiple transmissions in the same bandwidth, since multiple users can share the same spread spectrum bandwidth without interfering with one another — making these multiple access techniques.

You can learn more about spread spectrum communications at ScienceDirect (see References).

SIM800L GSM/GPRS Module

The SIM800L is a cellular module that allows for GPRS data transmission, sending and receiving SMS, and making and receiving voice calls. The board is compact and has low current consumption, with a power-saving technique that limits current draw to as low as 1 mA when idle. It supports quad-band GSM/GPRS network, meaning it works in most places around the world.

How Does It Work?

The module operates similarly to a cellular phone with a SIM card. After powering up, it boots, searches for the nearest cellular network, and connects automatically. The onboard LED indicates connection state — it blinks fast when there's no network coverage, and slow once connected.

DSM501A Dust Sensor

The DSM501A dust sensor module is a low-cost, compact particle density sensor 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 the device, allowing it to detect the reflected light of dust in the surrounding air. It's especially effective at 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, senses ozone gas (O3). It has high sensitivity to ozone while also being sensitive to strong oxides such as Cl2 and NO2. It's widely used in air quality monitoring applications, whether domestic or industrial.

How Does It Work?

Inside the black bakelite housing of the sensor is a sensitive material called Tungsten trioxide (WO3). Its conductivity is high in clean air, but drops when ozone gas is present in the surrounding air. This change in conductivity is converted into an output signal corresponding to the gas concentration detected.

04 Hardware Setup

Client (Ra-02 LoRa Module with Sensors)

Ra-02 LoRa Module Connections

Module Pin Connect To
3.3V Arduino 3.3V
RST Arduino pin 9
DIO0 Arduino pin 2
GND Arduino Ground
NSS Arduino pin 10
MOSI Arduino pin 11
MISO Arduino pin 12
SCK Arduino pin 13

DSM501A Dust Sensor Connections

Wire Connect To
3rd wire (VCC) Arduino 5V
4th wire (PM2.5) Arduino pin 7
5th wire (GND) Arduino Ground

MQ131 Ozone Gas Sensor Connections

Pin Connect To
A0 Arduino pin A0
VCC / GND Arduino 5V / Ground respectively

Server (Dragino LoRa Shield with the SIM800L Module)

The Dragino LoRa Shield plugs directly onto the second Arduino Uno as a shield — no manual pin wiring is required for the LoRa radio itself. The SIM800L module then connects to this Arduino for cellular connectivity.

Note: The original source didn't specify exact SIM800L pin connections in text (only a wiring photo, which isn't available here). Based on the server code's SoftwareSerial gsm(2, 3); line, the SIM800L's TX and RX lines connect to Arduino pins 2 and 3. Double-check your specific SIM800L breakout's pinout and power requirements (it typically needs a separate 3.7–4.2V supply capable of ~2A peak current, not the Arduino's 5V pin) before wiring.

05 ThingSpeak Setup

Follow these steps to set up your ThingSpeak channel before uploading the server code.

Step 1 — Sign Up and Create a Channel

Sign up at thingspeak.com. If you already have an account, log in and click "Channels", then click "New Channel" to create one.

Step 2 — Configure Channel Settings

Fill in the required Channel Settings and check 2 fields — one for each sensor reading (dust and ozone). You only need to give it a name and a description; the rest are unnecessary. When done, scroll to the bottom and click "Save Channel".

Step 3 — View and Customize Your Channel

You can view your channel privately by clicking the "Private View" tab at the top, and customize each field's chart by clicking its pencil icon.

Step 4 — Get Your API Key

You'll need your channel's write API key for the Arduino code so the SIM800L can transmit data to ThingSpeak. Find it under the "API Keys" tab at the top of your channel page.

06 Code

Read the Code Breakdown section to understand what each part does.

Client: AQM_Client.ino

Arduino / C++
#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: AQM_Server.ino

Arduino / C++
#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");
}
Note: Replace the API key in updateTS with your own ThingSpeak channel's write API key (from ThingSpeak Setup, Step 4) — and update the APN string ("http.globe.com.ph") to match your SIM card's carrier.

07 Code Breakdown

Client Code

Pre-Initialization

Arduino / C++
#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>
#include "DSM501.h"
#include "MQ131.h"

RHReliableDatagram sends addressed, acknowledged, and retransmitted datagrams, and depends on the RH_RF95 library. Both depend on SPI (Serial Peripheral Interface), so that's included too. DSM501 and MQ131 are the driver libraries for the dust sensor and ozone gas sensor respectively.

Arduino / C++
#define CLIENT_ADDRESS 123
#define SERVER_ADDRESS 321

RH_RF95 driver;
RHReliableDatagram manager(driver, CLIENT_ADDRESS);

This defines the client and server addresses, which is important to ensure you're communicating one-on-one with your own LoRa shield and not any others nearby. It then creates the RF95 driver object and datagram manager, using the driver and client address as parameters.

Arduino / C++
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];

This assigns pins to the sensors. For the DSM501, the first parameter is for PM1.0 (assigned to pin 6), and the second parameter is for PM2.5. For the MQ131, the first parameter is for the heater already integrated into the module (can be any unused pin), and the third parameter indicates the black bakelite low-concentration sensor version. 10000 is the load resistance. buf is initialized to store incoming messages.

setup()

Arduino / C++
Serial.begin(9600);
if (!manager.init())
  Serial.println("init failed");
dsm501.begin(MIN_WIN_SPAN);
o3sensor.setR0(252564.10); //set base resistance R0 value
o3sensor.setTimeToRead(30); //set time to read value 30s

This sets the serial baud rate to 9600 bps and checks whether the reliable datagram manager initialized successfully, printing an error if it didn't. It then initializes the DSM501 and MQ131 — the MQ131's base resistance was obtained by running o3sensor.calibrate() separately, and its read time is set to 30 seconds.

loop() — Dust Sensor Reading

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

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

This reads the dust sensor with dsm501.update(), then gets the particulate matter density for particles over 2.5 µm with dsm501.getParticleWeight(1). The float value is converted to a string with dtostrf().

Arduino / C++
if (manager.sendtoWait(data1, sizeof(data1), SERVER_ADDRESS)) {
  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);

This sends the data to the server with manager.sendtoWait(), which retries and waits for an acknowledgement. If an acknowledgement is received, it prints the reply with manager.recvfromAckTimeout().

loop() — Ozone Sensor Reading

Arduino / C++
o3sensor.begin();
Serial.print("Ozone Gas: ");
Serial.print(o3sensor.getO3(UG_M3));
Serial.println(" µg/m3\t");

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

This reads the ozone gas sensor with o3sensor.begin(), then reads the value in µg/m³ with o3sensor.getO3(UG_M3) (use o3sensor.getO3(PPB) instead for parts-per-billion, more common in global statistical reports). As with the dust reading, the value is converted to a string with dtostrf() and sent to the server the same way as the dust reading above.

Server Code

Pre-Initialization

Arduino / C++
#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>
#include <SoftwareSerial.h>

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

SoftwareSerial allows serial communication on other digital Arduino pins, letting pins 2 and 3 act as receiver and transmitter for the SIM800L. This also doubles as a debug serial to print the received message later.

Arduino / C++
#define CLIENT_ADDRESS 123
#define SERVER_ADDRESS 321

RH_RF95 driver;
RHReliableDatagram manager(driver, SERVER_ADDRESS);

As with the client, this defines the client and server addresses — make sure these match the client-side code exactly — then creates the RF95 driver object and datagram manager using the server address.

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

This creates the acknowledgement message sent back to the client, initializes buf to store incoming sensor data, and sets up index to distinguish between the two incoming data values (dust vs. ozone).

setup()

Arduino / C++
Serial.begin(9600);
gsm.begin(4800); // Set GSM Baudrate
if (!manager.init())
  Serial.println("init failed");

This sets the serial baud rate to 9600 bps and the GSM software-serial baud rate to 4800 bps, then checks whether the reliable datagram manager initialized successfully.

SIM800L AT Commands for Sending Data to ThingSpeak

Arduino / C++
void gsmSend() {
  String updateTS = "GET https://api.thingspeak.com/update?api_key=4Z6XNGVB0ZEGKNFJ&field";
  String updateTSvalue = updateTS + index + "=" + atof(buf);
  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);
  delay(2000);
  gsm.println("AT+CIPSHUT");
}

AT (ATtention) commands are instructions used to control a modem. AT+CIPMODE=0 sets the TCP/IP application mode to normal, needed to govern the SIM800L's internet connection. AT+CIPMUX=0 starts a single IP connection, and AT+CGATT=1 attaches to the GPRS service. AT+CSTT="APN" starts the task and sets the access point name (find the correct APN for your SIM card online), and AT+CIICR brings up the wireless GPRS connection.

AT+CIFSR gets the local IP address, and AT+CIPSTART="TCP","184.106.153.149","80" starts the TCP connection to ThingSpeak. AT+CIPSEND=80 allows sending about 80 characters of data — this is where the GET request runs. Make sure the API key in updateTS matches your channel's write API key (check the "API Keys" tab in ThingSpeak). AT+CIPSHUT must be sent at the end to deactivate GPRS — skipping it may cause the connection to time out on its own.

loop()

Arduino / C++
if (manager.available()) {
  if (index == 1) {
    uint8_t len = sizeof(buf);
    uint8_t from;
    if (manager.recvfromAck(buf, &len, &from))
    {
      Serial.println(buf);
      if (!manager.sendtoWait(data, sizeof(data), from))
        Serial.println("sendtoWait failed");
    }
    gsmSend();
    index = 2;
  }
  else if (index == 2) {
    uint8_t len = sizeof(buf);
    uint8_t from;
    if (manager.recvfromAck(buf, &len, &from))
    {
      Serial.println(buf);
      if (!manager.sendtoWait(data, sizeof(data), from))
        Serial.println("sendtoWait failed");
    }
    gsmSend();
    index = 1;
  }
}

This checks for an incoming message with manager.available(). If index is 1 (first sensor's data), recvfromAck() retrieves the data and prints it to the Serial Monitor, then calls gsmSend() to forward it to ThingSpeak and sets index to 2 so the next message is treated as the second sensor's data. The index == 2 branch mirrors this for the second sensor's reading, resetting index back to 1 afterward.

08 Conclusion

LoRa is a low data rate, low power wireless platform technology for building IoT networks. It's more cost-efficient in the long run compared to other alternatives due to its wide feature set — its transceivers can communicate up to 15 kilometers apart, which is more than enough for many applications. LoRa Technology operates in the unlicensed ISM band, making it legal and well-suited for educational purposes.

The sensors used for this project were a bit harder to test due to the difficulty of finding comparison values — you have to be fairly specific when searching online for air quality monitoring standards for dust particulate matter and ozone gas concentration. Fortunately, the libraries used for both sensors handle the underlying calculations.

09 Sensor Reading Comparison

These are the guidelines/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. See the WHO guideline document in References for the specific threshold values.

10 References

  • MQ131 Low Concentration Datasheet — GitHub (ostaquet/Arduino-MQ131-driver)
    (https://github.com/ostaquet/Arduino-MQ131-driver/blob/master/datasheet/MQ131-low-concentration.pdf)
  • SIM900 AT Command Manual — SIMCom
    (https://simcom.ee/documents/SIM900/SIM900_AT%20Command%20Manual_V1.11.pdf)
  • Spread Spectrum Communications — ScienceDirect
    (https://www.sciencedirect.com/topics/engineering/spread-spectrum-communications)
  • WHO Air Quality Guidelines — World Health Organization
    (https://apps.who.int/iris/bitstream/handle/10665/69477/WHO_SDE_PHE_OEH_06.02_eng.pdf?sequence=1)

11 Project Authors

  • Johnson Hong
Quality Checked by
  • Lukie-Mar Lonzaga
  • Miles Darren Bagnol
  • Elijah Joseph Orias
Remote Smart Air Quality Monitoring System Using LoRa, SIM800L, and ThingSpeak – CreateLabz

 

Air quality monitoring, Arduino, Dragino lora shield, Dsm501, Dsm501a, Dust sensor, Gprs, Gsm, Knowledgebase, Lora, Mq131, Ozone gas sensor, Ra-02, Remote, Rf95, Rhreliabledatagram, Rh_rf95, Sim800l, Spread spectrum communication, Sx1278, Thingspeak

Leave a comment

All comments are moderated before being published