The RAK12039 (which uses the Plantower PMSA003 sensor) is an excellent tool for measuring air quality in a classroom. It uses a laser to measure the concentration of particulate matter (PM) in the air, specifically PM1.0, PM2.5, and PM10.
In a classroom, these particles most commonly originate from:
- Dust (from floors, furniture, chalk)
- Pollen (from outside)
- Mold spores
- Outdoor pollution (especially if the school is near a busy street)
- Viruses and bacteria (which can "travel" on larger particles).
How can the RAK12039 be used in a classroom?
The RAK12039 is just a sensor. For it to work, it must be connected to a WisBlock Base board and a controller module (like the RAK11300 mentioned earlier).
Here is a typical usage scenario, step-by-step:
- Device Preparation: The sensor (RAK12039) and a controller (e.g., RAK11300 with LoRaWAN) are assembled into a single device.
- Strategic Placement: The device is placed in the classroom. The best location is approximately at the students' breathing height (1-1.5 meters), away from direct drafts (like an open window or fan) to ensure the measurements are as accurate as possible.
- Data Collection: The sensor periodically (e.g., every 5 or 10 minutes) "draws in" an air sample and uses its laser to read the amount of PM2.5 and PM10 particles ($\mu g/m^3$).
- Data Transmission: The RAK11300 module (or another controller with Wi-Fi) sends the collected data to an internet server or an Internet of Things (IoT) platform.
- Analysis and Alerts: The data is analyzed in real-time. Thresholds can be set (e.g., based on World Health Organization recommendations). If the PM2.5 level exceeds the norm, the system can: Send an alert to the teacher or administration (email, SMS). Activate a local indicator in the classroom (e.g., an LED light changes from green to red). Automatically turn on an air purifier (if one is present).
What benefits does this provide?
The primary benefit is a healthier learning environment. Particulate matter, especially PM2.5, is small enough to penetrate deep into the lungs and even enter the bloodstream.
Direct Benefits:
- Better Health: Reduced risk of respiratory problems for students (especially those with asthma or allergies). Poor air quality is also linked to increased rates of common illnesses.
- Improved Concentration: Studies show a direct correlation between poor air quality (high PM and CO2) and reduced cognitive function. When the air is cleaner, students are less drowsy, focus better, and achieve better learning outcomes.
- Problem Identification: The data helps to understand when and why the air quality deteriorates. Perhaps pollution is highest in the morning when children rush in? Or maybe after cleaning? This allows for targeted problem-solving.
"Green" Benefits (Efficiency and Sustainability):
The greatest "green" benefit is smart ventilation and energy savings.
- Traditional Ventilation: The teacher ventilates the classroom on a schedule (e.g., during every break), even if it's not necessary. In winter, a lot of expensive heat energy is lost through the open window.
- Smart Ventilation (with a sensor): The sensor shows when the air quality is actually poor. The room only needs to be ventilated when PM2.5 (or CO2, if also measured) levels rise above the norm.
The result: Ventilation is done on-demand rather than on a schedule. This significantly reduces heat loss during the cold season, thereby saving heating energy and lowering the school's expenses and CO.
Step 0: Gateway: Setup and Configuration
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 the WisBlock ComponentsInstall the core into the CPU_SLOT and secure it with screws, then we install the RAK12039 into the IO SLOT 2 and also secure it with screws and connect the PM2.5 sensor with a cable.
Carefully connect the LoRa 863-870MHz antenna to its designated location.
The hardware is assembled.
Step 2: Development Environment Configuration:1. Prepare the Boards Manager (RAK BSP)
Open File →Preferences.
In the Additional Boards Manager URLs field, enter the following address:
This is the official RAK Boards index.
Go to Tools → Board →Boards Manager…, search for RAK, and install the RAKwireless RP2040 (WisBlock Core RAK11300) BSP. This package adds the RAK11300 board to the IDE.
Select the board: Tools → Board → (RAKwireless / WisBlock) RAK11300.
2. Install the required libraries (Library Manager)
Open Tools → Manage Libraries… and install:
SX126x-Arduino - this is the main LoRa/LoRaWAN (v1.0.2) library for the SX1262 transmitter in the RAK11300 module. It is also intended for the RP2040. (Type "SX126x-Arduino" in the search.)
(Optional, but convenient) WisBlock-API - RAK WisBlock higher-level API (LoRa P2P, LoRaWAN, sensors, etc.). If you are using WisBlock baseboards and RAK sensors, this library simplifies your work.
If everything worked, then we upload the application code:
#include <Wire.h>
#include "RAK12039_PMSA003I.h"
#include "LoRaWan-Arduino.h"
RAK_PMSA003I PMSA003I;
#define SET_PIN WB_IO6
#define SENSOR_POWER WB_IO2
#define READ_INTERVAL 15000UL // 15 s
unsigned long lastRead = 0;
// LoRaWAN settings
bool doOTAA = true;
#define LORAWAN_APP_PORT 2
DeviceClass_t g_CurrentClass = CLASS_A;
LoRaMacRegion_t g_CurrentRegion = LORAMAC_REGION_EU868;
lmh_confirm g_CurrentConfirm = LMH_UNCONFIRMED_MSG;
uint8_t nodeDeviceEUI[8] = {0x00,0x00,0x00,0x00,0x00,0x06,0xB5,0xFB};
uint8_t nodeAppEUI[8] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88};
uint8_t nodeAppKey[16] = {0xA6,0x7D,0x91,0xEA,0x00,0xB8,0x6B,0x66,0x00,0x00,0x00,0xDC,0xDD,0x00,0x00,0xDD};
// Payload buffer: 6 particle counts (2 bytes each) + PM2.5 + PM10 = 16 bytes
#define LORAWAN_APP_DATA_BUFF_SIZE 16
static uint8_t m_lora_app_data_buffer[LORAWAN_APP_DATA_BUFF_SIZE];
static lmh_app_data_t m_lora_app_data = { m_lora_app_data_buffer, 0, 0, 0, 0 };
// LoRa minimal callbacks (paliekam paprastą skeletą)
static lmh_callback_t g_lora_callbacks = {
BoardGetBatteryLevel,
BoardGetUniqueId,
BoardGetRandomSeed,
nullptr, // rx
nullptr, // has_joined
nullptr, // class_confirm
nullptr, // join_failed
nullptr, // tx_finished / link_check (pagal API versiją gali būti ignoruojama)
nullptr
}; // <-- TRŪKO taškelio-kablelio
// Paprasta oro kokybės "etiketė"
const char* airQuality(uint16_t pm25, uint16_t pm10) {
if (pm25 <= 12 && pm10 <= 54) return "Good";
else if (pm25 <= 35 && pm10 <= 154) return "Moderate";
else if (pm25 <= 55 && pm10 <= 254) return "Unhealthy for sensitive";
else if (pm25 <= 150 && pm10 <= 354) return "Unhealthy";
else return "Very Unhealthy";
}
void setup() {
pinMode(SENSOR_POWER, OUTPUT);
digitalWrite(SENSOR_POWER, HIGH);
pinMode(SET_PIN, OUTPUT);
digitalWrite(SET_PIN, HIGH);
Serial.begin(115200);
delay(1000); // stabilize USB
Wire.begin();
delay(3000); // sensor power-up delay
if (!PMSA003I.begin()) {
Serial.println("PMSA003I initialization failed! Check wiring.");
while (1) delay(1000);
}
// LoRa init
lora_rak11300_init();
if (doOTAA) {
lmh_setDevEui(nodeDeviceEUI);
lmh_setAppEui(nodeAppEUI);
lmh_setAppKey(nodeAppKey);
}
lmh_param_t g_lora_param_init = {
LORAWAN_ADR_ON,
DR_0,
LORAWAN_PUBLIC_NETWORK,
3, // dutycycle / tx retries (pagal API – čia tinka)
TX_POWER_5,
LORAWAN_DUTYCYCLE_OFF
};
if (lmh_init(&g_lora_callbacks, g_lora_param_init, doOTAA, g_CurrentClass, g_CurrentRegion) != 0) {
Serial.println("lmh_init failed");
} else {
lmh_join(); // OTAA: pradėti join
}
}
void loop() {
unsigned long now = millis();
if (now - lastRead >= READ_INTERVAL) {
lastRead = now;
PMSA_Data_t data;
// BUG FIX: buvo "readDate" – turi būti "readData"
if (PMSA003I.readDate(&data)) {
// Serial Monitor output
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);
const char* aq = airQuality(data.pm25_env, data.pm100_env);
Serial.print("Air Quality: ");
Serial.println(aq);
// LoRa payload (16 bytes)
m_lora_app_data.buffer[0] = data.particles_03um >> 8;
m_lora_app_data.buffer[1] = data.particles_03um & 0xFF;
m_lora_app_data.buffer[2] = data.particles_05um >> 8;
m_lora_app_data.buffer[3] = data.particles_05um & 0xFF;
m_lora_app_data.buffer[4] = data.particles_10um >> 8;
m_lora_app_data.buffer[5] = data.particles_10um & 0xFF;
m_lora_app_data.buffer[6] = data.particles_25um >> 8;
m_lora_app_data.buffer[7] = data.particles_25um & 0xFF;
m_lora_app_data.buffer[8] = data.particles_50um >> 8;
m_lora_app_data.buffer[9] = data.particles_50um & 0xFF;
m_lora_app_data.buffer[10] = data.particles_100um >> 8;
m_lora_app_data.buffer[11] = data.particles_100um & 0xFF;
// PM2.5
m_lora_app_data.buffer[12] = data.pm25_env >> 8;
m_lora_app_data.buffer[13] = data.pm25_env & 0xFF;
// PM10
m_lora_app_data.buffer[14] = data.pm100_env >> 8;
m_lora_app_data.buffer[15] = data.pm100_env & 0xFF;
m_lora_app_data.port = LORAWAN_APP_PORT;
m_lora_app_data.buffsize = 16;
if (lmh_send(&m_lora_app_data, g_CurrentConfirm) == LMH_SUCCESS) {
Serial.println("Data sent via LoRaWAN");
} else {
Serial.println("LoRa send failed");
}
Serial.println();
} else {
Serial.println("PMSA003I read failed!");
}
}
}Javascript code for decoding data sent to TTN:
function decodeUplink(input) {
var bytes = input.bytes;
// Read particle counts (2 bytes each)
var particles_03um = (bytes[0] << 8) | bytes[1];
var particles_05um = (bytes[2] << 8) | bytes[3];
var particles_10um = (bytes[4] << 8) | bytes[5];
var particles_25um = (bytes[6] << 8) | bytes[7];
var particles_50um = (bytes[8] << 8) | bytes[9];
var particles_100um = (bytes[10] << 8) | bytes[11];
// PM values
var pm25 = (bytes[12] << 8) | bytes[13];
var pm10 = (bytes[14] << 8) | bytes[15];
// Air quality evaluation
var airQuality = "Unknown";
if(pm25 <= 12 && pm10 <= 54) airQuality = "Good";
else if(pm25 <= 35 && pm10 <= 154) airQuality = "Moderate";
else if(pm25 <= 55 && pm10 <= 254) airQuality = "Unhealthy for sensitive";
else if(pm25 <= 150 && pm10 <= 354) airQuality = "Unhealthy";
else airQuality = "Very Unhealthy";
// Return in the same order as Serial Monitor
return {
data: {
"The number of particles in 0.1L air (above diameter)": {
"0.3um": particles_03um,
"0.5um": particles_05um,
"1.0um": particles_10um,
"2.5um": particles_25um,
"5.0um": particles_50um,
"10um": particles_100um
},
"Air Quality": airQuality,
"PM2.5": pm25,
"PM10": pm10
}
};
}Live Data received:
Test how the decoding of the received data works.
Latest decoded payload:









Comments