Air quality has a significant impact on human health, productivity, and environmental comfort. In enclosed spaces, poor ventilation and invisible pollutants such as VOCs and elevated CO₂ levels can go unnoticed while contributing to fatigue, respiratory problems, and reduced cognitive performance. Most monitoring systems today are either expensive, designed for outdoor use, or lack real-time responsiveness to indoor conditions.
This project introduces a compact, IoT-based indoor air quality system that combines the RAK1906 (BME680) and RAK12004 (SCD30) sensors, integrated with the RAK4631 WisBlock Core and LoRaWAN connectivity. The system provides a deeper understanding of indoor environments by pairing general air quality estimation with direct CO₂ monitoring.
Combining Two Sensors for Greater Insight
- RAK1906 captures VOC levels, temperature, humidity, and pressure. This data is used to estimate indoor air quality (IAQ), dew point, discomfort index, and heat index—key indicators for human comfort and pollution detection.
- RAK12004 offers precise CO₂ concentration measurements, which are essential for evaluating ventilation efficiency and identifying human occupancy trends.
By combining both sensors, this system can not only detect that air quality is declining, but also understand the cause—whether from chemical pollutants, human presence, or inadequate airflow. This adds a layer of intelligence not possible with either sensor alone.
Real-World Applications
Smart Classrooms Detects rising CO₂ that impacts student focus, while also tracking VOCs from cleaning products or materials to ensure a healthier learning space.
Smart Offices Monitors indoor pollution and ventilation status, helping optimize HVAC systems and improving employee wellness and productivity.
Smart Homes Identifies stale air or VOC exposure from everyday activities like cooking and cleaning. Can trigger fans or alerts based on both CO₂ and IAQ thresholds.
Indoor Agriculture Tracks CO₂ for growth optimization while ensuring the environment remains stable and free from harmful gases released by soil or fertilizers.
Things Used in the Project
Hardware Components
- Microcontroller & Sensors:
- RAK4631 WisBlock Core – LoRa-enabled microcontroller for wireless IoT connectivity.
- RAK1906 Environmental Sensor – Measures air quality (VOCs), temperature, humidity, and pressure.
- RAK12004 (MQ2) – MQ2 gas sensor, sensitive to LPG, butane, propane, methane, alcohol, hydrogen, smoke, and other flammable steam.
- RAK19007 WisBlock Base Board – Provides power and connection slots for WisBlock modules.
- Connectivity:
- RAK7268 WisGate Edge Lite 2 – LoRaWAN gateway to transmit sensor data to The Things Network (TTN).
- LoRa Antenna – Enhances long-range wireless communication between the device and the gateway.
- Power Supply:
- USB-C Cable (for programming and powering the device)
- Rechargeable Li-ion battery (for standalone operation)
- Other Components:
- Jumper wires, connectors
- 3D-printed enclosure (optional)
Software Apps and Online Services
- Programming & Development:
- Arduino IDE (for coding and uploading firmware)
- RAK WisBlock Libraries (for sensor integration)
- IoT & Cloud Data Management:
- The Things Network (TTN) (for LoRaWAN connectivity)
- Ubidots (for data visualization & analysis)
- Additional Tools:
- GitHub (for source code storage & collaboration)
Hand Tools and Fabrication Machines
- Screwdrivers (for assembling sensors)
- Pliers & wire strippers (for electrical connections)
- 3D printer (optional, for custom enclosures)
To avoid damage to the gateway, make sure to connect the antenna before turning it on!
This step is thoroughly explained in the guide - IoT Education Kit - Setup the Gateway RAK7268V2 - that can be found on https://www.hackster.io/520073/iot-education-kit-setup-the-gateway-rak7268v2-6b222f
Step 1: Mounting WisBlock Parts- Attach RAK4630 WisBlock Core onto the RAK19007 WisBlock Base.
- Connect RAK1906 (Air Quality Sensor) to an I²C slot.
- Connect RAK12004 (SCD30) to another I²C slot. Gently push the WQ-2 sensor into the Rak 12004 before connecting to the board.
- Connect the LoRa Antenna
The full instructions for setting up the Arduino IDE and connecting your WisBlock RAK4631 to The Things Network (TTN) are covered in detail in the guide: Getting Started with WisBlock and The Things Network (TTN)
Follow that guide to
- Install board support for WisBlock in the Arduino
- Load and configure the LoRaWAN example sketch
- Register your device on TTN
- Insert TTN credentials into your code
- Upload the code and verify data transmission
Once complete, your device will be ready to send data over LoRaWAN through TTN!
Step 4: Verifying the RAK1906 sensorBelow is a step-by-step guide showing how to verify the RAK1906_Environment_BME690 sensor.
Part 1: Installing the library
In this example we tested a sensor which is RAK1906 and, for that, you will need to follow the same steps above to install the library, but the specific library for our example is SX126x-Arduino.
Part 2: Setting Up the Sensor and Serial Monitor
1. Open the Example for the Sensor:
- Open Arduino IDE and go to:
`File > Examples > RAK WisBlock Examples > RAK 4631 > Sensors > RAK1906_Environment_BME690 `.
2. View Data in Serial Monitor:
Below is a step-by-step guide to verifying the RAK12019 Ambient Light Sensor and ensuring it provides accurate luminosity readings.
Part 1: Installing the Library
To use the RAK12019 sensor, you need to install the appropriate library. Follow the steps below:
Open Arduino IDE.
Go to Tools > Manage Libraries.
In the search bar, type Rak12004
Click the Install button next to the library.
Then, in the search bar, type U8g2
Click the Install button next to the library.
Part 2: Setting Up the Sensor and Serial Monitor
1. Open the Example for the Sensor:
In Arduino IDE, navigate to:File > Examples > RAK WisBlock Examples > RAK 4631 > IO > RAK12004
.
2. View Data in Serial Monitor:
- Select the correct board: WisBlock RAK4631.
- Ensure the correct COM port is selected.
- Click Verify & Upload to send the code to the board.
- Open the Serial Monitor (Tools > Serial Monitor) and set the baud rate to 115200.
- If the sensor is working correctly, the monitor should display real-time luminosity readings in lux (lx).
1. Configure LoRaWAN Credentials
Ensure your RAK4631 device can join The Things Network (TTN) by setting the correct credentials:
- Obtain Device Credentials:
- From the TTN console, retrieve your device's Device EUI, Application EUI, and App Key.
- Update the Example Code:
- Open the RAK4631_DeepSleep_LoRaWAN example.
- Locate the arrays for nodeDeviceEUI[8], nodeAppEUI[8], and nodeAppKey[16].
2. Initialize Sensors in setup()
Integrate the BME680 and LTR390 sensor initialization into the setup() function:
- Include Necessary Libraries:
At the beginning of your code, include the sensor libraries:
#include <Wire.h>
#include "ADC121C021.h" // Click to install library: http://librarymanager/All#MQx
#include <U8g2lib.h> // Click to install library: http://librarymanager/All#u8g2
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h>
#define EN_PIN WB_IO6 //Logic high enables the device. Logic low disables the device
#define ALERT_PIN WB_IO5 //a high indicates that the respective limit has been violated.
#define MQ2_ADDRESS 0x51 //the device i2c address
#define RatioMQ2CleanAir (1.0) //RS / R0 = 1.0 ppm
#define MQ2_RL (10.0) //the board RL = 10KΩ can adjust
uint16_t result;
char displayData[32]; //OLED dispaly datas
//Function declaration
void firstDisplay();
- Declare Sensor Objects:
Before the setup() function, declare the sensor objects:
ADC121C021 MQ2;
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0);
Adafruit_BME680 bme;
float sensorPPM;
float PPMpercentage;
And the following functions:
void bme680_init()
void bme680_get()
void firstDisplay()
- Initialize Sensors:
Within the setup() function, initialize both sensors. After the Serial insert this:
while (!Serial)
{
if ((millis() - serial_timeout) < 5000)
{
delay(100);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
else
{
break;
}
}
#endif
bme680_init();
u8g2.begin();
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
sprintf(displayData, "MQ2 checking...");
u8g2.drawStr(3, 45, displayData);
u8g2.sendBuffer();
//********ADC121C021 ADC convert init ********************************
while(!(MQ2.begin(MQ2_ADDRESS,Wire)))
{
Serial.println("please check device!!!");
delay(200);
}
Serial.println("RAK12004 test Example");
//**************init MQ2 *****************************************************
MQ2.setRL(MQ2_RL);
/*
*detect Propane gas if to detect other gas need to reset A and B value,it depend on MQ sensor datasheet
*/
MQ2.setA(-0.890); //A -> Slope, -0.685
MQ2.setB(1.125); //B -> Intersect with X - Axis 1.019
//Set math model to calculate the PPM concentration and the value of constants
MQ2.setRegressionMethod(0); //PPM = pow(10, (log10(ratio)-B)/A)
float calcR0 = 0;
for(int i = 1; i<=100; i ++)
{
calcR0 += MQ2.calibrateR0(RatioMQ2CleanAir);
}
MQ2.setR0(calcR0/100);
if(isinf(calcR0)) {Serial.println("Warning: Conection issue founded, R0 is infite (Open circuit detected) please check your wiring and supply"); while(1);}
if(calcR0 == 0){Serial.println("Warning: Conection issue founded, R0 is zero (Analog pin with short circuit to ground) please check your wiring and supply"); while(1);}
float r0 = MQ2.getR0();
Serial.printf("R0 Value is:%3.2f\r\n",r0);
firstDisplay();
delay(3000);
3. Collect Sensor Data in loop()
Modify the loop() function to read data from both sensors and prepare it for transmission:
- Read Sensor Data:
Within the loop() function, read data from the BME680 and LTR390 sensors. After this line:
/// \todo read sensor or whatever you need to do frequently
Insert this code:
Serial.println("Getting Conversion Readings from ADC121C021");
Serial.println(" ");
sensorPPM = MQ2.readSensor();
Serial.printf("sensor PPM Value is: %3.2f\r\n",sensorPPM);
PPMpercentage = sensorPPM/10000;
Serial.printf("PPM percentage Value is:%3.2f%%\r\n",PPMpercentage);
Serial.println(" ");
Serial.println(" *************************** ");
Serial.println(" ");
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
sprintf(displayData, "Propane:");
u8g2.drawStr(3, 30, displayData);
sprintf(displayData, "%3.2f PPM",sensorPPM);
u8g2.drawStr(3, 45, displayData);
sprintf(displayData, "%3.2f %%",PPMpercentage);
u8g2.drawStr(3, 60, displayData);
u8g2.sendBuffer();
delay(1000);
if (! bme.performReading())
{
Serial.println("Failed to perform reading :(");
}
bme680_get();
delay(5000);
4. Update the SendLoraFrame Function
On the main file and on the lora_handler.cpp you should update the new SendLoraFrame function to include the new readings in this case this would be:
if (sendLoRaFrame(bme.temperature, bme.humidity, bme.pressure, bme.gas_resistance, sensorPPM, PPMpercentage ))
After you should convert sensor readings into a byte array suitable for LoRaWAN transmission:
bool sendLoRaFrame(
float temperature,
float pressure,
float humidity,
float gas_resistance,
float sensorPPM,
float PPMpercentage
)
{
if (lmh_join_status_get() != LMH_SET)
{
//Not joined, try again later
#ifndef MAX_SAVE
Serial.println("Did not join network, skip sending frame");
#endif
return false;
}
m_lora_app_data.port = LORAWAN_APP_PORT;
//******************************************************************
/// \todo here some more usefull data should be put into the package
//******************************************************************
uint8_t buffSize = 0;
// Convert values to uint16_t (scaled)
uint16_t temp = (uint16_t)((temperature * 100) + 5000); // allows negatives
uint16_t hum = (uint16_t)((humidity * 100) + 5000);
uint16_t press = (uint16_t)(pressure); // Already in hPa
uint16_t gas = (uint16_t)(gas_resistance); // Already in KΩ
uint16_t ppm = (uint16_t)(sensorPPM * 100); // 356.78 → 35678
uint16_t ppm_percent = (uint16_t)(PPMpercentage * 100); // 0.08 → 8
// Add each value to payload with unique IDs
m_lora_app_data_buffer[buffSize++] = 0x01; // Temperature
m_lora_app_data_buffer[buffSize++] = temp >> 8;
m_lora_app_data_buffer[buffSize++] = temp & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x02; // Humidity
m_lora_app_data_buffer[buffSize++] = hum >> 8;
m_lora_app_data_buffer[buffSize++] = hum & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x03; // Pressure
m_lora_app_data_buffer[buffSize++] = press >> 8;
m_lora_app_data_buffer[buffSize++] = press & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x04; // Gas Resistance
m_lora_app_data_buffer[buffSize++] = gas >> 8;
m_lora_app_data_buffer[buffSize++] = gas & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x05; // Propane PPM
m_lora_app_data_buffer[buffSize++] = ppm >> 8;
m_lora_app_data_buffer[buffSize++] = ppm & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x06; // Propane PPM %
m_lora_app_data_buffer[buffSize++] = ppm_percent >> 8;
m_lora_app_data_buffer[buffSize++] = ppm_percent & 0xFF;
// Set buffer size
m_lora_app_data.buffsize = buffSize;
lmh_error_status error = lmh_send(&m_lora_app_data, LMH_UNCONFIRMED_MSG);
return (error == 0);
}
The full working code can be found on github: https://github.com/1ClickBait/DSIOT/tree/main/RAK4631_1906_12004
You can find the full example of the.ino file here:
/**
* @file RAK4631-DeepSleep-LoRaWan.ino
* @author Bernd Giesecke (bernd.giesecke@rakwireless.com)
* @brief LoRaWan deep sleep example
* Device goes into sleep after successful OTAA/ABP network join.
* Wake up every SLEEP_TIME seconds. Set time in main.h
* @version 0.1
* @date 2020-09-05
*
* @copyright Copyright (c) 2020
*
* @note RAK4631 GPIO mapping to nRF52840 GPIO ports
RAK4631 <-> nRF52840
WB_IO1 <-> P0.17 (GPIO 17)
WB_IO2 <-> P1.02 (GPIO 34)
WB_IO3 <-> P0.21 (GPIO 21)
WB_IO4 <-> P0.04 (GPIO 4)
WB_IO5 <-> P0.09 (GPIO 9)
WB_IO6 <-> P0.10 (GPIO 10)
WB_SW1 <-> P0.01 (GPIO 1)
WB_A0 <-> P0.04/AIN2 (AnalogIn A2)
WB_A1 <-> P0.31/AIN7 (AnalogIn A7)
*/
#include "main.h"
#include <Wire.h>
#include "ADC121C021.h" // Click to install library: http://librarymanager/All#MQx
#include <U8g2lib.h> // Click to install library: http://librarymanager/All#u8g2
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h> // Click to install library: http://librarymanager/All#Adafruit_BME680
Adafruit_BME680 bme;
// Might need adjustments
#define SEALEVELPRESSURE_HPA (1010.0)
float sensorPPM;
float PPMpercentage;
void bme680_init()
{
Wire.begin();
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME680 sensor, check wiring!");
return;
}
// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms
}
void bme680_get()
{
Serial.print("Temperature = ");
Serial.print(bme.temperature);
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.pressure / 100.0);
Serial.println(" hPa");
Serial.print("Humidity = ");
Serial.print(bme.humidity);
Serial.println(" %");
Serial.print("Gas = ");
Serial.print(bme.gas_resistance / 1000.0);
Serial.println(" KOhms");
Serial.println();
}
#define EN_PIN WB_IO6 //Logic high enables the device. Logic low disables the device
#define ALERT_PIN WB_IO5 //a high indicates that the respective limit has been violated.
#define MQ2_ADDRESS 0x51 //the device i2c address
#define RatioMQ2CleanAir (1.0) //RS / R0 = 1.0 ppm
#define MQ2_RL (10.0) //the board RL = 10KΩ can adjust
ADC121C021 MQ2;
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0);
uint16_t result;
char displayData[32]; //OLED dispaly datas
//Function declaration
void firstDisplay();
void firstDisplay()
{
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
u8g2.sendBuffer();
sprintf(displayData, "R0:%3.3f", MQ2.getR0());
u8g2.drawStr(3, 30, displayData);
u8g2.sendBuffer();
float voltage = MQ2.getSensorVoltage();
sprintf(displayData, "voltage:%3.3f",voltage);
u8g2.drawStr(3, 45, displayData);
u8g2.sendBuffer();
}
/** Semaphore used by events to wake up loop task */
SemaphoreHandle_t taskEvent = NULL;
/** Timer to wakeup task frequently and send message */
SoftwareTimer taskWakeupTimer;
/** Buffer for received LoRaWan data */
uint8_t rcvdLoRaData[256];
/** Length of received data */
uint8_t rcvdDataLen = 0;
/**
* @brief Flag for the event type
* -1 => no event
* 0 => LoRaWan data received
* 1 => Timer wakeup
* 2 => tbd
* ...
*/
uint8_t eventType = -1;
/**
* @brief Timer event that wakes up the loop task frequently
*
* @param unused
*/
void periodicWakeup(TimerHandle_t unused)
{
// Switch on blue LED to show we are awake
digitalWrite(LED_BUILTIN, HIGH);
eventType = 1;
// Give the semaphore, so the loop task will wake up
xSemaphoreGiveFromISR(taskEvent, pdFALSE);
}
/**
* @brief Arduino setup function. Called once after power-up or reset
*
*/
void setup(void)
{
// Create the LoRaWan event semaphore
taskEvent = xSemaphoreCreateBinary();
// Initialize semaphore
xSemaphoreGive(taskEvent);
// Initialize the built in LED
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
// Initialize the connection status LED
pinMode(LED_CONN, OUTPUT);
digitalWrite(LED_CONN, HIGH);
#ifndef MAX_SAVE
// Initialize Serial for debug output
Serial.begin(115200);
time_t timeout = millis();
// On nRF52840 the USB serial is not available immediately
while (!Serial)
{
if ((millis() - timeout) < 5000)
{
delay(100);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
else
{
break;
}
}
#endif
bme680_init();
u8g2.begin();
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
sprintf(displayData, "MQ2 checking...");
u8g2.drawStr(3, 45, displayData);
u8g2.sendBuffer();
//********ADC121C021 ADC convert init ********************************
while(!(MQ2.begin(MQ2_ADDRESS,Wire)))
{
Serial.println("please check device!!!");
delay(200);
}
Serial.println("RAK12004 test Example");
//**************init MQ2 *****************************************************
MQ2.setRL(MQ2_RL);
/*
*detect Propane gas if to detect other gas need to reset A and B value,it depend on MQ sensor datasheet
*/
MQ2.setA(-0.890); //A -> Slope, -0.685
MQ2.setB(1.125); //B -> Intersect with X - Axis 1.019
//Set math model to calculate the PPM concentration and the value of constants
MQ2.setRegressionMethod(0); //PPM = pow(10, (log10(ratio)-B)/A)
float calcR0 = 0;
for(int i = 1; i<=100; i ++)
{
calcR0 += MQ2.calibrateR0(RatioMQ2CleanAir);
}
MQ2.setR0(calcR0/100);
if(isinf(calcR0)) {Serial.println("Warning: Conection issue founded, R0 is infite (Open circuit detected) please check your wiring and supply"); while(1);}
if(calcR0 == 0){Serial.println("Warning: Conection issue founded, R0 is zero (Analog pin with short circuit to ground) please check your wiring and supply"); while(1);}
float r0 = MQ2.getR0();
Serial.printf("R0 Value is:%3.2f\r\n",r0);
firstDisplay();
delay(3000);
#ifndef MAX_SAVE
Serial.println("=====================================");
Serial.println("RAK4631 LoRaWan Deep Sleep Test");
Serial.println("=====================================");
#endif
// Initialize LoRaWan and start join request
int8_t loraInitResult = initLoRaWan();
#ifndef MAX_SAVE
if (loraInitResult != 0)
{
switch (loraInitResult)
{
case -1:
Serial.println("SX126x init failed");
break;
case -2:
Serial.println("LoRaWan init failed");
break;
case -3:
Serial.println("Subband init error");
break;
case -4:
Serial.println("LoRa Task init error");
break;
default:
Serial.println("LoRa init unknown error");
break;
}
// Without working LoRa we just stop here
while (1)
{
Serial.println("Nothing I can do, just loving you");
delay(5000);
}
}
Serial.println("LoRaWan init success");
#endif
// Take the semaphore so the loop will go to sleep until an event happens
xSemaphoreTake(taskEvent, 10);
}
/**
* @brief Arduino loop task. Called in a loop from the FreeRTOS task handler
*
*/
void loop(void)
{
// Switch off blue LED to show we go to sleep
digitalWrite(LED_BUILTIN, LOW);
// Sleep until we are woken up by an event
if (xSemaphoreTake(taskEvent, portMAX_DELAY) == pdTRUE)
{
// Switch on blue LED to show we are awake
digitalWrite(LED_BUILTIN, HIGH);
delay(500); // Only so we can see the blue LED
// Check the wake up reason
switch (eventType)
{
case 0: // Wakeup reason is package downlink arrived
#ifndef MAX_SAVE
Serial.println("Received package over LoRaWan");
#endif
if (rcvdLoRaData[0] > 0x1F)
{
#ifndef MAX_SAVE
Serial.printf("%s\n", (char *)rcvdLoRaData);
#endif
}
else
{
#ifndef MAX_SAVE
for (int idx = 0; idx < rcvdDataLen; idx++)
{
Serial.printf("%X ", rcvdLoRaData[idx]);
}
Serial.println("");
#endif
}
break;
case 1: // Wakeup reason is timer
#ifndef MAX_SAVE
Serial.println("Timer wakeup");
#endif
/// \todo read sensor or whatever you need to do frequently
Serial.println("Getting Conversion Readings from ADC121C021");
Serial.println(" ");
sensorPPM = MQ2.readSensor();
Serial.printf("sensor PPM Value is: %3.2f\r\n",sensorPPM);
PPMpercentage = sensorPPM/10000;
Serial.printf("PPM percentage Value is:%3.2f%%\r\n",PPMpercentage);
Serial.println(" ");
Serial.println(" *************************** ");
Serial.println(" ");
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
sprintf(displayData, "Propane:");
u8g2.drawStr(3, 30, displayData);
sprintf(displayData, "%3.2f PPM",sensorPPM);
u8g2.drawStr(3, 45, displayData);
sprintf(displayData, "%3.2f %%",PPMpercentage);
u8g2.drawStr(3, 60, displayData);
u8g2.sendBuffer();
delay(1000);
if (! bme.performReading())
{
Serial.println("Failed to perform reading :(");
}
bme680_get();
delay(5000);
// Send the data package
if (sendLoRaFrame(bme.temperature,bme.humidity,bme.pressure,bme.gas_resistance, sensorPPM, PPMpercentage ))
{
#ifndef MAX_SAVE
Serial.println("LoRaWan package sent successfully");
#endif
}
else
{
#ifndef MAX_SAVE
Serial.println("LoRaWan package send failed");
/// \todo maybe you need to retry here?
#endif
}
break;
default:
#ifndef MAX_SAVE
Serial.println("This should never happen ;-)");
#endif
break;
}
digitalWrite(LED_BUILTIN, LOW);
// Go back to sleep
xSemaphoreTake(taskEvent, 10);
}
}
You can find the example of the.cpp file here:
/**
* @file RAK4631-DeepSleep-LoRaWan.ino
* @author Bernd Giesecke (bernd.giesecke@rakwireless.com)
* @brief LoRaWan deep sleep example
* Device goes into sleep after successful OTAA/ABP network join.
* Wake up every SLEEP_TIME seconds. Set time in main.h
* @version 0.1
* @date 2020-09-05
*
* @copyright Copyright (c) 2020
*
* @note RAK4631 GPIO mapping to nRF52840 GPIO ports
RAK4631 <-> nRF52840
WB_IO1 <-> P0.17 (GPIO 17)
WB_IO2 <-> P1.02 (GPIO 34)
WB_IO3 <-> P0.21 (GPIO 21)
WB_IO4 <-> P0.04 (GPIO 4)
WB_IO5 <-> P0.09 (GPIO 9)
WB_IO6 <-> P0.10 (GPIO 10)
WB_SW1 <-> P0.01 (GPIO 1)
WB_A0 <-> P0.04/AIN2 (AnalogIn A2)
WB_A1 <-> P0.31/AIN7 (AnalogIn A7)
*/
#include "main.h"
#include <Wire.h>
#include "ADC121C021.h" // Click to install library: http://librarymanager/All#MQx
#include <U8g2lib.h> // Click to install library: http://librarymanager/All#u8g2
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h> // Click to install library: http://librarymanager/All#Adafruit_BME680
Adafruit_BME680 bme;
// Might need adjustments
#define SEALEVELPRESSURE_HPA (1010.0)
float sensorPPM;
float PPMpercentage;
void bme680_init()
{
Wire.begin();
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME680 sensor, check wiring!");
return;
}
// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms
}
void bme680_get()
{
Serial.print("Temperature = ");
Serial.print(bme.temperature);
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.pressure / 100.0);
Serial.println(" hPa");
Serial.print("Humidity = ");
Serial.print(bme.humidity);
Serial.println(" %");
Serial.print("Gas = ");
Serial.print(bme.gas_resistance / 1000.0);
Serial.println(" KOhms");
Serial.println();
}
#define EN_PIN WB_IO6 //Logic high enables the device. Logic low disables the device
#define ALERT_PIN WB_IO5 //a high indicates that the respective limit has been violated.
#define MQ2_ADDRESS 0x51 //the device i2c address
#define RatioMQ2CleanAir (1.0) //RS / R0 = 1.0 ppm
#define MQ2_RL (10.0) //the board RL = 10KΩ can adjust
ADC121C021 MQ2;
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0);
uint16_t result;
char displayData[32]; //OLED dispaly datas
//Function declaration
void firstDisplay();
void firstDisplay()
{
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
u8g2.sendBuffer();
sprintf(displayData, "R0:%3.3f", MQ2.getR0());
u8g2.drawStr(3, 30, displayData);
u8g2.sendBuffer();
float voltage = MQ2.getSensorVoltage();
sprintf(displayData, "voltage:%3.3f",voltage);
u8g2.drawStr(3, 45, displayData);
u8g2.sendBuffer();
}
/** Semaphore used by events to wake up loop task */
SemaphoreHandle_t taskEvent = NULL;
/** Timer to wakeup task frequently and send message */
SoftwareTimer taskWakeupTimer;
/** Buffer for received LoRaWan data */
uint8_t rcvdLoRaData[256];
/** Length of received data */
uint8_t rcvdDataLen = 0;
/**
* @brief Flag for the event type
* -1 => no event
* 0 => LoRaWan data received
* 1 => Timer wakeup
* 2 => tbd
* ...
*/
uint8_t eventType = -1;
/**
* @brief Timer event that wakes up the loop task frequently
*
* @param unused
*/
void periodicWakeup(TimerHandle_t unused)
{
// Switch on blue LED to show we are awake
digitalWrite(LED_BUILTIN, HIGH);
eventType = 1;
// Give the semaphore, so the loop task will wake up
xSemaphoreGiveFromISR(taskEvent, pdFALSE);
}
/**
* @brief Arduino setup function. Called once after power-up or reset
*
*/
void setup(void)
{
// Create the LoRaWan event semaphore
taskEvent = xSemaphoreCreateBinary();
// Initialize semaphore
xSemaphoreGive(taskEvent);
// Initialize the built in LED
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
// Initialize the connection status LED
pinMode(LED_CONN, OUTPUT);
digitalWrite(LED_CONN, HIGH);
#ifndef MAX_SAVE
// Initialize Serial for debug output
Serial.begin(115200);
time_t timeout = millis();
// On nRF52840 the USB serial is not available immediately
while (!Serial)
{
if ((millis() - timeout) < 5000)
{
delay(100);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
else
{
break;
}
}
#endif
bme680_init();
u8g2.begin();
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
sprintf(displayData, "MQ2 checking...");
u8g2.drawStr(3, 45, displayData);
u8g2.sendBuffer();
//********ADC121C021 ADC convert init ********************************
while(!(MQ2.begin(MQ2_ADDRESS,Wire)))
{
Serial.println("please check device!!!");
delay(200);
}
Serial.println("RAK12004 test Example");
//**************init MQ2 *****************************************************
MQ2.setRL(MQ2_RL);
/*
*detect Propane gas if to detect other gas need to reset A and B value,it depend on MQ sensor datasheet
*/
MQ2.setA(-0.890); //A -> Slope, -0.685
MQ2.setB(1.125); //B -> Intersect with X - Axis 1.019
//Set math model to calculate the PPM concentration and the value of constants
MQ2.setRegressionMethod(0); //PPM = pow(10, (log10(ratio)-B)/A)
float calcR0 = 0;
for(int i = 1; i<=100; i ++)
{
calcR0 += MQ2.calibrateR0(RatioMQ2CleanAir);
}
MQ2.setR0(calcR0/100);
if(isinf(calcR0)) {Serial.println("Warning: Conection issue founded, R0 is infite (Open circuit detected) please check your wiring and supply"); while(1);}
if(calcR0 == 0){Serial.println("Warning: Conection issue founded, R0 is zero (Analog pin with short circuit to ground) please check your wiring and supply"); while(1);}
float r0 = MQ2.getR0();
Serial.printf("R0 Value is:%3.2f\r\n",r0);
firstDisplay();
delay(3000);
#ifndef MAX_SAVE
Serial.println("=====================================");
Serial.println("RAK4631 LoRaWan Deep Sleep Test");
Serial.println("=====================================");
#endif
// Initialize LoRaWan and start join request
int8_t loraInitResult = initLoRaWan();
#ifndef MAX_SAVE
if (loraInitResult != 0)
{
switch (loraInitResult)
{
case -1:
Serial.println("SX126x init failed");
break;
case -2:
Serial.println("LoRaWan init failed");
break;
case -3:
Serial.println("Subband init error");
break;
case -4:
Serial.println("LoRa Task init error");
break;
default:
Serial.println("LoRa init unknown error");
break;
}
// Without working LoRa we just stop here
while (1)
{
Serial.println("Nothing I can do, just loving you");
delay(5000);
}
}
Serial.println("LoRaWan init success");
#endif
// Take the semaphore so the loop will go to sleep until an event happens
xSemaphoreTake(taskEvent, 10);
}
/**
* @brief Arduino loop task. Called in a loop from the FreeRTOS task handler
*
*/
void loop(void)
{
// Switch off blue LED to show we go to sleep
digitalWrite(LED_BUILTIN, LOW);
// Sleep until we are woken up by an event
if (xSemaphoreTake(taskEvent, portMAX_DELAY) == pdTRUE)
{
// Switch on blue LED to show we are awake
digitalWrite(LED_BUILTIN, HIGH);
delay(500); // Only so we can see the blue LED
// Check the wake up reason
switch (eventType)
{
case 0: // Wakeup reason is package downlink arrived
#ifndef MAX_SAVE
Serial.println("Received package over LoRaWan");
#endif
if (rcvdLoRaData[0] > 0x1F)
{
#ifndef MAX_SAVE
Serial.printf("%s\n", (char *)rcvdLoRaData);
#endif
}
else
{
#ifndef MAX_SAVE
for (int idx = 0; idx < rcvdDataLen; idx++)
{
Serial.printf("%X ", rcvdLoRaData[idx]);
}
Serial.println("");
#endif
}
break;
case 1: // Wakeup reason is timer
#ifndef MAX_SAVE
Serial.println("Timer wakeup");
#endif
/// \todo read sensor or whatever you need to do frequently
Serial.println("Getting Conversion Readings from ADC121C021");
Serial.println(" ");
sensorPPM = MQ2.readSensor();
Serial.printf("sensor PPM Value is: %3.2f\r\n",sensorPPM);
PPMpercentage = sensorPPM/10000;
Serial.printf("PPM percentage Value is:%3.2f%%\r\n",PPMpercentage);
Serial.println(" ");
Serial.println(" *************************** ");
Serial.println(" ");
u8g2.clearDisplay();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB10_tr); // choose a suitable font
memset(displayData, 0, sizeof(displayData));
sprintf(displayData, "RAK12004 Test");
u8g2.drawStr(3, 15, displayData);
sprintf(displayData, "Propane:");
u8g2.drawStr(3, 30, displayData);
sprintf(displayData, "%3.2f PPM",sensorPPM);
u8g2.drawStr(3, 45, displayData);
sprintf(displayData, "%3.2f %%",PPMpercentage);
u8g2.drawStr(3, 60, displayData);
u8g2.sendBuffer();
delay(1000);
if (! bme.performReading())
{
Serial.println("Failed to perform reading :(");
}
bme680_get();
delay(5000);
// Send the data package
if (sendLoRaFrame(bme.temperature,bme.humidity,bme.pressure,bme.gas_resistance, sensorPPM, PPMpercentage ))
{
#ifndef MAX_SAVE
Serial.println("LoRaWan package sent successfully");
#endif
}
else
{
#ifndef MAX_SAVE
Serial.println("LoRaWan package send failed");
/// \todo maybe you need to retry here?
#endif
}
break;
default:
#ifndef MAX_SAVE
Serial.println("This should never happen ;-)");
#endif
break;
}
digitalWrite(LED_BUILTIN, LOW);
// Go back to sleep
xSemaphoreTake(taskEvent, 10);
}
}
You can find the main file here:
/**
* @file main.h
* @author Bernd Giesecke (bernd.giesecke@rakwireless.com)
* @brief Includes, definitions and global declarations for DeepSleep example
* @version 0.1
* @date 2020-08-15
*
* @copyright Copyright (c) 2020
*
*/
#include <Arduino.h>
#include <SPI.h>
#include <LoRaWan-RAK4630.h>
// Comment the next line if you want DEBUG output. But the power savings are not as good then!!!!!!!
#define MAX_SAVE
/* Time the device is sleeping in milliseconds = 2 minutes * 60 seconds * 1000 milliseconds */
#define SLEEP_TIME 2 * 60 * 1000
// LoRaWan stuff
int8_t initLoRaWan(void);
bool sendLoRaFrame(
float temperature,
float pressure,
float humidity,
float gas_resistance,
float sensorPPM,
float PPMpercentage
);
extern SemaphoreHandle_t loraEvent;
// Main loop stuff
void periodicWakeup(TimerHandle_t unused);
extern SemaphoreHandle_t taskEvent;
extern uint8_t rcvdLoRaData[];
extern uint8_t rcvdDataLen;
extern uint8_t eventType;
extern SoftwareTimer taskWakeupTimer;
Step 7: Adding Payload Formatters in TTN1. Set Up a Custom Payload Formatter:
- When data is transmitted via LoRaWAN, it is often in a raw, unreadable format. Payload Formatters in TTN help convert these raw values into human-readable data (like JSON).
- In TTN, navigate to the Payload Formatter section of your application.
- Choose the Uplink option and select Custom JavaScript Formatter from the Formatter Type dropdown.
- Write a JavaScript function to "unpack" the data received from the device, reversing any conversions you made before sending:
function Decoder(bytes, port) {
var decoded = {};
for (var i = 0; i < bytes.length; i += 3) {
var type = bytes[i];
var value = (bytes[i + 1] << 8) | bytes[i + 2];
switch (type) {
case 0x01: // Temperature
decoded.Temperature_C = (value - 5000) / 100;
break;
case 0x02: // Humidity
decoded.Humidity = (value - 5000) / 100;
break;
case 0x03: // Pressure
decoded.Pressure = value;
break;
case 0x04: // Gas Resistance
decoded.Gas_Resistance = value;
break;
case 0x05: // CO2 ppm (from RAK12004 / SCD30)
decoded.CO2_ppm = value / 100;
break;
case 0x06: // CO2 % (optional)
decoded.CO2_Percent = value / 100;
break;
default:
decoded["Unknown_0x" + type.toString(16)] = value;
break;
}
}
return decoded;
}
2. Test and Save:
- On the same page, TTN provides fields to test your script with sample data. Input a test payload and verify the decoded output.
- Once the script works as expected, save it.
3. Monitor Data in TTN:
In this final step, the goal is to complete the pipeline by:
Forwarding your sensor data from TTN to Ubidots using a webhook
Decoding the payload so the values can be understood and used by Ubidots
Visualizing the data on a live dashboard
Creating automated alerts (events) based on sensor conditions
This part transforms your raw IoT data into actionable insights, allowing you to monitor environmental conditions and respond to them in real time (e.g., send a warning when gas levels drop or lights turn on).
We’ve already covered how to format and decode the payload using a custom function, but the full setup—connecting TTN to Ubidots, configuring the webhook, setting up the dashboard, and building alerts—is covered step-by-step in the following external guide:
Step-by-Step Guide to Setting Up Webhooks in TTN and Integrating with Ubidots
Please refer to that guide to:
Set up your Ubidots account and plugin
Create and configure your webhook in TTN
Use the decoding function in Ubidots
Build visual dashboards and event triggers for your data
Once you've followed those instructions, your system will be fully functional—receiving live sensor readings, displaying them visually, and responding to environmental changes through alerts and automation.
Comments
Please log in or sign up to comment.