Air quality plays a crucial role in both human health and environmental stability. Among the most harmful pollutants are fine airborne particles—PM1.0, PM2.5, and PM10—which are small enough to penetrate deep into the lungs and bloodstream, causing respiratory and cardiovascular issues. However, particle concentration alone doesn’t tell the full story. Environmental factors like temperature and humidity significantly influence how these particles behave, travel, and accumulate in both indoor and outdoor settings.
This project introduces a Smart IoT-Based Air Quality Monitoring System that combines the RAK12039 Particulate Matter Sensor (PMSA003I) with the RAK1906 Environmental Sensor, which provides temperature, humidity, and atmospheric pressure data. Both sensors are integrated into the RAK4631 WisBlock Core and transmit real-time data via LoRaWAN to The Things Network (TTN). The collected data is then visualized and analyzed using Ubidots, enabling remote access, historical tracking, and customizable alerts.
Why Combine Particulate Matter with Temperature & Humidity?
- Particle Behavior: High humidity can cause particles to absorb water, increasing in size and changing in chemical composition—affecting health risk and sensor detection.
- Pollutant Dispersion: Temperature and pressure influence vertical air movement, which controls how pollutants disperse or concentrate in a specific area.
- Indoor vs Outdoor Air: By measuring environmental conditions alongside PM levels, users can identify sources of pollution, whether they’re outdoor emissions or indoor activities (e.g., cooking, cleaning, or machinery).
Real-World Applications
- Public Health & Community Monitoring
- Identify high-risk periods for vulnerable populations (e.g., during heatwaves with high PM2.5).
- Detect pollutant accumulation in low-ventilation environments like classrooms or elderly care facilities.
- Smart Agriculture
- Monitor dust and air quality in greenhouses and outdoor farms.
- Analyze how temperature and humidity interact with particle movement and plant sensitivity.
- Urban Planning & Traffic Management
- Place sensors near intersections or industrial zones to correlate pollution with traffic volume and weather conditions.
- Use the data to inform emissions regulations or vehicle flow changes.
- Indoor Air Quality for Buildings
- Deploy in smart homes, schools, or offices to detect unhealthy air.
- Trigger HVAC adjustments or alerts when PM or humidity levels spike.
- Environmental Research & Climate Studies
- Track seasonal variations in air quality.
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 RAK12039 Particulate Matter Sensor to an IO slot.
- 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 RAK12039 and ensuring it provides accurate luminosity readings.
Part 1: Installing the Library
Follow the steps below:
Open Arduino IDE.
Go to Tools > Manage Libraries.
In the search bar, type Rak12039
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 > RAK12039_Dust_Read
.
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).
Above is the outcome in a normal environment and you can check its working because when we put the sensor in a dusty environment we will have expected changes in the results like we confirmed in the next picture.
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 "RAK12039_PMSA003I.h"
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h>
- Declare Sensor Objects:
Before the setup() function, declare the sensor objects:
RAK_PMSA003I PMSA003I;
Adafruit_BME680 bme;
// Might need adjustments
#define SEALEVELPRESSURE_HPA (1010.0)
/*
* @brief WB_IO6 is connected to the SET pin.
* Set pin/TTL level @3.3V, high level or suspending is normal working status.
* while low level is sleeping mode.
*/
#define SET_PIN WB_IO6
- Declare the functions
Declare the following functions:
void bme680_get()
void bme680_init()
- Initialize Sensors:
Within the setup() function, initialize both sensors. After the Serial insert this:
bme680_init();
Wire.begin();
if(!PMSA003I.begin())
{
Serial.println("PMSA003I begin fail,please check connection!");
delay(100);
while(1);
}
Before make sure you power the sensor:
// Sensor power on.
pinMode(WB_IO2, OUTPUT);
digitalWrite(WB_IO2, HIGH);
pinMode(SET_PIN, OUTPUT);
digitalWrite(SET_PIN, HIGH);
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:
/// \todo read sensor or whatever you need to do frequently
if (PMSA003I.readDate(&data))
{
Serial.println("PMSA003I read date success.");
Serial.println("Standard particulate matter:");
Serial.print("PM1.0: ");
Serial.print(data.pm10_standard);
Serial.println(" [µg/𝑚3]");
Serial.print("PM2.5: ");
Serial.print(data.pm25_standard);
Serial.println(" [µg/𝑚3]");
Serial.print("PM10 : ");
Serial.print(data.pm100_standard);
Serial.println(" [µg/𝑚3]");
Serial.println("Atmospheric environment:");
Serial.print("PM1.0: ");
Serial.print(data.pm10_env);
Serial.println(" [µg/𝑚3]");
Serial.print("PM2.5: ");
Serial.print(data.pm25_env);
Serial.println(" [µg/𝑚3]");
Serial.print("PM10 : ");
Serial.print(data.pm100_env);
Serial.println(" [µg/𝑚3]");
Serial.println("The number of particles in 0.1L air (above diameter):");
Serial.print("0.3um:");
Serial.println(data.particles_03um);
Serial.print("0.5um:");
Serial.println(data.particles_05um);
Serial.print("1.0um:");
Serial.println(data.particles_10um);
Serial.print("2.5um:");
Serial.println(data.particles_25um);
Serial.print("5.0um:");
Serial.println(data.particles_50um);
Serial.print("10 um:");
Serial.println(data.particles_100um);
}
else
{
Serial.println("PMSA003I read failed!");
}
Serial.println();
delay(1000);
if (! bme.performReading())
{
Serial.println("Failed to perform reading :(");
}
bme680_get();
delay(5000);
// Send the data package
if (sendLoRaFrame(bme.temperature,bme.pressure,bme.humidity,bme.gas_resistance,data.pm10_standard,data.pm25_standard,data.pm100_standard,data.pm10_env,data.pm25_env,data.pm100_env,data.particles_03um,data.particles_05um,data.particles_10um,data.particles_25um,data.particles_50um,data.particles_100um))
{
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 following: float temperature, float pressure, float humidity, float gas_resistance, uint16_t pm10_std, uint16_t pm25_std, uint16_t pm100_std, uint16_t pm10_env, uint16_t pm25_env, uint16_t pm100_env, uint16_t part03, uint16_t part05, uint16_t part10, uint16_t part25, uint16_t part50, uint16_t part100
After you should convert sensor readings into a byte array suitable for LoRaWAN transmission:
// Reset buffer size
uint8_t buffSize = 0;
buffSize = 0;
uint16_t temp = (uint16_t)(temperature * 100);
uint16_t press = (uint16_t)(pressure / 100); // Pa → hPa
uint16_t hum = (uint16_t)(humidity * 100);
uint16_t gas = (uint16_t)(gas_resistance / 1000);
// Add each measurement with its own ID
m_lora_app_data_buffer[buffSize++] = 0x01; // Temp
m_lora_app_data_buffer[buffSize++] = temp >> 8;
m_lora_app_data_buffer[buffSize++] = temp & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x02; // Pressure
m_lora_app_data_buffer[buffSize++] = press >> 8;
m_lora_app_data_buffer[buffSize++] = press & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x03; // Humidity
m_lora_app_data_buffer[buffSize++] = hum >> 8;
m_lora_app_data_buffer[buffSize++] = hum & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x04; // Gas
m_lora_app_data_buffer[buffSize++] = gas >> 8;
m_lora_app_data_buffer[buffSize++] = gas & 0xFF;
// Standard PM
m_lora_app_data_buffer[buffSize++] = 0x10; // PM1.0 std
m_lora_app_data_buffer[buffSize++] = pm10_std >> 8;
m_lora_app_data_buffer[buffSize++] = pm10_std & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x11; // PM2.5 std
m_lora_app_data_buffer[buffSize++] = pm25_std >> 8;
m_lora_app_data_buffer[buffSize++] = pm25_std & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x12; // PM10 std
m_lora_app_data_buffer[buffSize++] = pm100_std >> 8;
m_lora_app_data_buffer[buffSize++] = pm100_std & 0xFF;
// Env PM
m_lora_app_data_buffer[buffSize++] = 0x13; // PM1.0 env
m_lora_app_data_buffer[buffSize++] = pm10_env >> 8;
m_lora_app_data_buffer[buffSize++] = pm10_env & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x14; // PM2.5 env
m_lora_app_data_buffer[buffSize++] = pm25_env >> 8;
m_lora_app_data_buffer[buffSize++] = pm25_env & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x15; // PM10 env
m_lora_app_data_buffer[buffSize++] = pm100_env >> 8;
m_lora_app_data_buffer[buffSize++] = pm100_env & 0xFF;
// Particles by size
m_lora_app_data_buffer[buffSize++] = 0x20;
m_lora_app_data_buffer[buffSize++] = part03 >> 8;
m_lora_app_data_buffer[buffSize++] = part03 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x21;
m_lora_app_data_buffer[buffSize++] = part05 >> 8;
m_lora_app_data_buffer[buffSize++] = part05 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x22;
m_lora_app_data_buffer[buffSize++] = part10 >> 8;
m_lora_app_data_buffer[buffSize++] = part10 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x23;
m_lora_app_data_buffer[buffSize++] = part25 >> 8;
m_lora_app_data_buffer[buffSize++] = part25 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x24;
m_lora_app_data_buffer[buffSize++] = part50 >> 8;
m_lora_app_data_buffer[buffSize++] = part50 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x25;
m_lora_app_data_buffer[buffSize++] = part100 >> 8;
m_lora_app_data_buffer[buffSize++] = part100 & 0xFF;
The code is on github page, you can find the project here: https://github.com/1ClickBait/DSIOT/tree/main/RAK4631_12039_1906
You can also copy the.ino file from 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 "RAK12039_PMSA003I.h"
#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)
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();
}
RAK_PMSA003I PMSA003I;
/*
* @brief WB_IO6 is connected to the SET pin.
* Set pin/TTL level @3.3V, high level or suspending is normal working status.
* while low level is sleeping mode.
*/
#define SET_PIN WB_IO6
/** 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);
// Sensor power on.
pinMode(WB_IO2, OUTPUT);
digitalWrite(WB_IO2, HIGH);
pinMode(SET_PIN, OUTPUT);
digitalWrite(SET_PIN, 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();
Wire.begin();
if(!PMSA003I.begin())
{
Serial.println("PMSA003I begin fail,please check connection!");
delay(100);
while(1);
}
digitalWrite(LED_BUILTIN, LOW);
#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)
{
PMSA_Data_t data;
// 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
if (PMSA003I.readDate(&data))
{
Serial.println("PMSA003I read date success.");
Serial.println("Standard particulate matter:");
Serial.print("PM1.0: ");
Serial.print(data.pm10_standard);
Serial.println(" [µg/𝑚3]");
Serial.print("PM2.5: ");
Serial.print(data.pm25_standard);
Serial.println(" [µg/𝑚3]");
Serial.print("PM10 : ");
Serial.print(data.pm100_standard);
Serial.println(" [µg/𝑚3]");
Serial.println("Atmospheric environment:");
Serial.print("PM1.0: ");
Serial.print(data.pm10_env);
Serial.println(" [µg/𝑚3]");
Serial.print("PM2.5: ");
Serial.print(data.pm25_env);
Serial.println(" [µg/𝑚3]");
Serial.print("PM10 : ");
Serial.print(data.pm100_env);
Serial.println(" [µg/𝑚3]");
Serial.println("The number of particles in 0.1L air (above diameter):");
Serial.print("0.3um:");
Serial.println(data.particles_03um);
Serial.print("0.5um:");
Serial.println(data.particles_05um);
Serial.print("1.0um:");
Serial.println(data.particles_10um);
Serial.print("2.5um:");
Serial.println(data.particles_25um);
Serial.print("5.0um:");
Serial.println(data.particles_50um);
Serial.print("10 um:");
Serial.println(data.particles_100um);
}
else
{
Serial.println("PMSA003I read failed!");
}
Serial.println();
delay(1000);
if (! bme.performReading())
{
Serial.println("Failed to perform reading :(");
}
bme680_get();
delay(5000);
// Send the data package
if (sendLoRaFrame(bme.temperature,bme.pressure,bme.humidity,bme.gas_resistance,data.pm10_standard,data.pm25_standard,data.pm100_standard,data.pm10_env,data.pm25_env,data.pm100_env,data.particles_03um,data.particles_05um,data.particles_10um,data.particles_25um,data.particles_50um,data.particles_100um))
{
#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);
}
}
A ciopy of the.cpp here:
/**
@file lora_handler.cpp
@author Bernd Giesecke (bernd.giesecke@rakwireless.com)
@brief Initialization, event handlers and task for LoRaWan
@version 0.1
@date 2020-08-15
@copyright Copyright (c) 2020
*/
#include "main.h"
/** DIO1 GPIO pin for RAK4631 */
#define PIN_LORA_DIO_1 47
/** Max size of the data to be transmitted. */
#define LORAWAN_APP_DATA_BUFF_SIZE 64
/** Number of trials for the join request. */
#define JOINREQ_NBTRIALS 8
/** Lora application data buffer. */
static uint8_t m_lora_app_data_buffer[LORAWAN_APP_DATA_BUFF_SIZE];
/** Lora application data structure. */
static lmh_app_data_t m_lora_app_data = {m_lora_app_data_buffer, 0, 0, 0, 0};
// LoRaWan event handlers
/** LoRaWan callback when join network finished */
static void lorawan_has_joined_handler(void);
/** LoRaWan callback when join failed */
static void lorawan_join_failed_handler(void);
/** LoRaWan callback when data arrived */
static void lorawan_rx_handler(lmh_app_data_t *app_data);
/** LoRaWan callback after class change request finished */
static void lorawan_confirm_class_handler(DeviceClass_t Class);
/** LoRaWan Function to send a package */
bool sendLoRaFrame(void);
/**@brief Structure containing LoRaWan parameters, needed for lmh_init()
Set structure members to
LORAWAN_ADR_ON or LORAWAN_ADR_OFF to enable or disable adaptive data rate
LORAWAN_DEFAULT_DATARATE OR DR_0 ... DR_5 for default data rate or specific data rate selection
LORAWAN_PUBLIC_NETWORK or LORAWAN_PRIVATE_NETWORK to select the use of a public or private network
JOINREQ_NBTRIALS or a specific number to set the number of trials to join the network
LORAWAN_DEFAULT_TX_POWER or a specific number to set the TX power used
LORAWAN_DUTYCYCLE_ON or LORAWAN_DUTYCYCLE_OFF to enable or disable duty cycles
Please note that ETSI mandates duty cycled transmissions.
*/
static lmh_param_t lora_param_init = {LORAWAN_ADR_OFF, DR_3, LORAWAN_PUBLIC_NETWORK, JOINREQ_NBTRIALS, LORAWAN_DEFAULT_TX_POWER, LORAWAN_DUTYCYCLE_OFF};
/** Structure containing LoRaWan callback functions, needed for lmh_init() */
static lmh_callback_t lora_callbacks = {BoardGetBatteryLevel, BoardGetUniqueId, BoardGetRandomSeed,
lorawan_rx_handler, lorawan_has_joined_handler, lorawan_confirm_class_handler, lorawan_join_failed_handler
};
// !!!! KEYS ARE MSB !!!!
/** Device EUI required for OTAA network join */
uint8_t nodeDeviceEUI[8] = {0xAC, 0x1F, 0x09, 0xFF, 0xFE, 0x16, 0x20, 0x3C};
/** Application EUI required for network join */
uint8_t nodeAppEUI[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
/** Application key required for network join */
uint8_t nodeAppKey[16] = {0x2C, 0xB6, 0x36, 0x02, 0xAF, 0x55, 0x8A, 0x86, 0x43, 0x33, 0x01, 0x29, 0xAA, 0x5D, 0xB5, 0x4B};
/** Device address required for ABP network join */
uint32_t nodeDevAddr = 0x26021FB6;
/** Network session key required for ABP network join */
uint8_t nodeNwsKey[16] = {0x32, 0x3D, 0x15, 0x5A, 0x00, 0x0D, 0xF3, 0x35, 0x30, 0x7A, 0x16, 0xDA, 0x0C, 0x9D, 0xF5, 0x3F};
/** Application session key required for ABP network join */
uint8_t nodeAppsKey[16] = {0x3F, 0x6A, 0x66, 0x45, 0x9D, 0x5E, 0xDC, 0xA6, 0x3C, 0xBC, 0x46, 0x19, 0xCD, 0x61, 0xA1, 0x1E};
/** Flag whether to use OTAA or ABP network join method */
bool doOTAA = true;
DeviceClass_t gCurrentClass = CLASS_A; /* class definition*/
LoRaMacRegion_t gCurrentRegion = LORAMAC_REGION_EU868; /* Region:EU868*/
/**
@brief Initialize LoRa HW and LoRaWan MAC layer
@return int8_t result
0 => OK
-1 => SX126x HW init failure
-2 => LoRaWan MAC initialization failure
-3 => Subband selection failure
*/
int8_t initLoRaWan(void)
{
// Initialize LoRa chip.
if (lora_rak4630_init() != 0)
{
return -1;
}
// Setup the EUIs and Keys
if (doOTAA)
{
lmh_setDevEui(nodeDeviceEUI);
lmh_setAppEui(nodeAppEUI);
lmh_setAppKey(nodeAppKey);
}
else
{
lmh_setNwkSKey(nodeNwsKey);
lmh_setAppSKey(nodeAppsKey);
lmh_setDevAddr(nodeDevAddr);
}
// Initialize LoRaWan
if (lmh_init(&lora_callbacks, lora_param_init, doOTAA, gCurrentClass, gCurrentRegion) != 0)
{
return -2;
}
// For some regions we might need to define the sub band the gateway is listening to
// This must be called AFTER lmh_init()
if (!lmh_setSubBandChannels(1))
{
return -3;
}
// Start Join procedure
#ifndef MAX_SAVE
Serial.println("Start network join request");
#endif
lmh_join();
return 0;
}
/**
@brief LoRa function for handling HasJoined event.
*/
static void lorawan_has_joined_handler(void)
{
if (doOTAA)
{
uint32_t otaaDevAddr = lmh_getDevAddr();
#ifndef MAX_SAVE
Serial.printf("OTAA joined and got dev address %08X\n", otaaDevAddr);
#endif
}
else
{
#ifndef MAX_SAVE
Serial.println("ABP joined");
#endif
}
// Default is Class A, where the SX1262 transceiver is in sleep mode unless a package is sent
// If switched to Class C the power consumption is higher because the SX1262 chip remains in RX mode
// lmh_class_request(CLASS_C);
digitalWrite(LED_CONN, LOW);
// Now we are connected, start the timer that will wakeup the loop frequently
taskWakeupTimer.begin(SLEEP_TIME, periodicWakeup);
taskWakeupTimer.start();
}
/**@brief LoRa function for handling OTAA join failed
*/
static void lorawan_join_failed_handler(void)
{
Serial.println("OVER_THE_AIR_ACTIVATION failed!");
Serial.println("Check your EUI's and Keys's!");
Serial.println("Check if a Gateway is in range!");
}
/**
@brief Function for handling LoRaWan received data from Gateway
@param app_data Pointer to rx data
*/
static void lorawan_rx_handler(lmh_app_data_t *app_data)
{
#ifndef MAX_SAVE
Serial.printf("LoRa Packet received on port %d, size:%d, rssi:%d, snr:%d\n",
app_data->port, app_data->buffsize, app_data->rssi, app_data->snr);
#endif
switch (app_data->port)
{
case 3:
// Port 3 switches the class
if (app_data->buffsize == 1)
{
switch (app_data->buffer[0])
{
case 0:
lmh_class_request(CLASS_A);
#ifndef MAX_SAVE
Serial.println("Request to switch to class A");
#endif
break;
case 1:
lmh_class_request(CLASS_B);
#ifndef MAX_SAVE
Serial.println("Request to switch to class B");
#endif
break;
case 2:
lmh_class_request(CLASS_C);
#ifndef MAX_SAVE
Serial.println("Request to switch to class C");
#endif
break;
default:
break;
}
}
break;
case LORAWAN_APP_PORT:
// Copy the data into loop data buffer
memcpy(rcvdLoRaData, app_data->buffer, app_data->buffsize);
rcvdDataLen = app_data->buffsize;
eventType = 0;
// Notify task about the event
if (taskEvent != NULL)
{
#ifndef MAX_SAVE
Serial.println("Waking up loop task");
#endif
xSemaphoreGive(taskEvent);
}
}
}
/**
@brief Callback for class switch confirmation
@param Class The new class
*/
static void lorawan_confirm_class_handler(DeviceClass_t Class)
{
#ifndef MAX_SAVE
Serial.printf("switch to class %c done\n", "ABC"[Class]);
#endif
// Informs the server that switch has occurred ASAP
m_lora_app_data.buffsize = 0;
m_lora_app_data.port = LORAWAN_APP_PORT;
lmh_send(&m_lora_app_data, LMH_UNCONFIRMED_MSG);
}
/**
@brief Send a LoRaWan package
@return result of send request
*/
bool sendLoRaFrame(
float temperature,
float pressure,
float humidity,
float gas_resistance,
uint16_t pm10_std,
uint16_t pm25_std,
uint16_t pm100_std,
uint16_t pm10_env,
uint16_t pm25_env,
uint16_t pm100_env,
uint16_t part03,
uint16_t part05,
uint16_t part10,
uint16_t part25,
uint16_t part50,
uint16_t part100
)
{
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
//******************************************************************
// Reset buffer size
uint8_t buffSize = 0;
buffSize = 0;
uint16_t temp = (uint16_t)(temperature * 100);
uint16_t press = (uint16_t)(pressure / 100); // Pa → hPa
uint16_t hum = (uint16_t)(humidity * 100);
uint16_t gas = (uint16_t)(gas_resistance / 1000);
// Add each measurement with its own ID
m_lora_app_data_buffer[buffSize++] = 0x01; // Temp
m_lora_app_data_buffer[buffSize++] = temp >> 8;
m_lora_app_data_buffer[buffSize++] = temp & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x02; // Pressure
m_lora_app_data_buffer[buffSize++] = press >> 8;
m_lora_app_data_buffer[buffSize++] = press & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x03; // Humidity
m_lora_app_data_buffer[buffSize++] = hum >> 8;
m_lora_app_data_buffer[buffSize++] = hum & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x04; // Gas
m_lora_app_data_buffer[buffSize++] = gas >> 8;
m_lora_app_data_buffer[buffSize++] = gas & 0xFF;
// Standard PM
m_lora_app_data_buffer[buffSize++] = 0x10; // PM1.0 std
m_lora_app_data_buffer[buffSize++] = pm10_std >> 8;
m_lora_app_data_buffer[buffSize++] = pm10_std & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x11; // PM2.5 std
m_lora_app_data_buffer[buffSize++] = pm25_std >> 8;
m_lora_app_data_buffer[buffSize++] = pm25_std & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x12; // PM10 std
m_lora_app_data_buffer[buffSize++] = pm100_std >> 8;
m_lora_app_data_buffer[buffSize++] = pm100_std & 0xFF;
// Env PM
m_lora_app_data_buffer[buffSize++] = 0x13; // PM1.0 env
m_lora_app_data_buffer[buffSize++] = pm10_env >> 8;
m_lora_app_data_buffer[buffSize++] = pm10_env & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x14; // PM2.5 env
m_lora_app_data_buffer[buffSize++] = pm25_env >> 8;
m_lora_app_data_buffer[buffSize++] = pm25_env & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x15; // PM10 env
m_lora_app_data_buffer[buffSize++] = pm100_env >> 8;
m_lora_app_data_buffer[buffSize++] = pm100_env & 0xFF;
// Particles by size
m_lora_app_data_buffer[buffSize++] = 0x20;
m_lora_app_data_buffer[buffSize++] = part03 >> 8;
m_lora_app_data_buffer[buffSize++] = part03 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x21;
m_lora_app_data_buffer[buffSize++] = part05 >> 8;
m_lora_app_data_buffer[buffSize++] = part05 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x22;
m_lora_app_data_buffer[buffSize++] = part10 >> 8;
m_lora_app_data_buffer[buffSize++] = part10 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x23;
m_lora_app_data_buffer[buffSize++] = part25 >> 8;
m_lora_app_data_buffer[buffSize++] = part25 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x24;
m_lora_app_data_buffer[buffSize++] = part50 >> 8;
m_lora_app_data_buffer[buffSize++] = part50 & 0xFF;
m_lora_app_data_buffer[buffSize++] = 0x25;
m_lora_app_data_buffer[buffSize++] = part100 >> 8;
m_lora_app_data_buffer[buffSize++] = part100 & 0xFF;
m_lora_app_data.buffsize = buffSize;
lmh_error_status error = lmh_send(&m_lora_app_data, LMH_UNCONFIRMED_MSG);
return (error == 0);
}
And a copy of the main 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,
uint16_t pm10_std,
uint16_t pm25_std,
uint16_t pm100_std,
uint16_t pm10_env,
uint16_t pm25_env,
uint16_t pm100_env,
uint16_t part03,
uint16_t part05,
uint16_t part10,
uint16_t part25,
uint16_t part50,
uint16_t part100
);
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 (adjusting scale)
decoded.Temp_C = (value - 5000) / 100;
break;
case 0x02: // Pressure (hPa)
decoded.Pressure = value * 100;
break;
case 0x03: // Humidity (adjusting scale)
decoded.Humidity = (value - 5000) / 100;
break;
case 0x04: // Gas resistance (kΩ)
decoded.Gas_Resistance = value * 1000;
break;
case 0x10:
decoded.PM1_0_Standard = value;
break;
case 0x11:
decoded.PM2_5_Standard = value;
break;
case 0x12:
decoded.PM10_Standard = value;
break;
case 0x13:
decoded.PM1_0_Env = value;
break;
case 0x14:
decoded.PM2_5_Env = value;
break;
case 0x15:
decoded.PM10_Env = value;
break;
case 0x20:
decoded.Particles_0_3um = value;
break;
case 0x21:
decoded.Particles_0_5um = value;
break;
case 0x22:
decoded.Particles_1_0um = value;
break;
case 0x23:
decoded.Particles_2_5um = value;
break;
case 0x24:
decoded.Particles_5_0um = value;
break;
case 0x25:
decoded.Particles_10_0um = value;
break;
default:
decoded.Unknown = value; // Just in case an unexpected byte is found
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.