Miguel Montiel Vega
Published

IoT Weather and UV lIight with LoRaWAN and The Thind

This system monitors environmental conditions and UV radiation in real time using low-power IoT sensors. It transmits data via LoRaWAN to Th

IntermediateWork in progress8 hours114
IoT Weather and UV lIight with LoRaWAN and The Thind

Things used in this project

Hardware components

rak 1907
×1
RAK4631
×1
 RAK12030
×1
 RAK12019
×1
RAK1921
×1

Software apps and online services

Arduino IDE
Arduino IDE
The Things Stack
The Things Industries The Things Stack

Hand tools and fabrication machines

o Screwdriver

Story

Read more

Code

IoT Weather and Air Quality Station

C/C++
// Project : IoT Weather and Air Quality Station
// Board: RAK4631 (Nordic NRF52840)
// Base: RAK1907
// Sensors: BME680, LTR-390UV-01, Rain Sensor, PMSA003I
// Display: RAK1821 OLED
// LoRaWAN Platform: The Things Network

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h>          // For BME680
#include <Adafruit_GFX.h>             // For OLED
#include <Adafruit_SSD1306.h>         // For OLED
#include <SparkFun_PMSA003I_Arduino_Library.h> // For PMSA003I
#include <Adafruit_LTR390.h>         // For LTR-390UV-01
#include <LoRaWan-RAK4631.h>          // LoRaWAN library for RAK4631
#include <SPI.h>

// LoRaWAN Configuration (OTAA) - REPLACE WITH YOUR THE THINGS NETWORK CREDENTIALS!
uint8_t nodeDeviceEUI[8] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}; // DevEUI
uint8_t nodeAppEUI[8] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX};   // AppEUI / JoinEUI
uint8_t nodeAppKey[16] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}; // AppKey

// LoRaWAN region configuration (e.g., RAK_REGION_EU868, RAK_REGION_US915)
#define LORAWAN_REGION RAK_REGION_EU868
#define LORAWAN_DEVEUI nodeDeviceEUI
#define LORAWAN_APPEUI nodeAppEUI
#define LORAWAN_APPKEY nodeAppKey
#define LORAWAN_CHANNEL_MASK NULL
#define LORAWAN_PREAMBLE_LENGTH 8
#define LORAWAN_ADR_STATE LORAWAN_ADR_ON
#define LORAWAN_TX_POWER LORAWAN_TX_POWER_0
#define LORAWAN_DATARATE LORAWAN_DR_3
#define LORAWAN_PUBLIC_NETWORK true
#define LORAWAN_DUTYCYCLE_ON false // Set to true for production to comply with regulations
#define LORAWAN_APP_PORT 1          // LoRaWAN application port

// Sensor Pins
#define RAIN_SENSOR_PIN WB_A0 // Analog pin for the rain sensor
#define OLED_RESET -1         // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_WIDTH 128      // OLED display width, in pixels
#define SCREEN_HEIGHT 64      // OLED display height, in pixels

// Sensor instances
Adafruit_BME680 bme;             // I2C
Adafruit_LTR390 ltr;             // I2C
PMSA003I airSensor;              // I2C for PMSA003I
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // I2C for OLED

// Variables to store sensor readings
float temperature, humidity, pressure, gas_resistance;
uint16_t pm1_0, pm2_5, pm10_0;
float uv_index, ambient_light;
bool is_raining;

void setup() {
  Serial.begin(115200);
  delay(100);
  Serial.println("Starting IoT Weather Station...");

  // Initialize I2C
  Wire.begin();

  // Initialize OLED
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed, loop forever
  }
  display.display();
  delay(2000);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Initializing...");
  display.display();

  // Initialize BME680
  if (!bme.begin(0x76)) { // Default I2C address for BME680
    Serial.println("Could not find BME680 sensor. Check connections!");
    display.clearDisplay(); display.setCursor(0, 0); display.println("BME680 Error!"); display.display();
    while (1);
  }
  bme.setTemperatureOversampling(BME680_OVERSAMPLING_8X);
  bme.setHumidityOversampling(BME680_OVERSAMPLING_4X);
  bme.setPressureOversampling(BME680_OVERSAMPLING_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150); // 320 C for 150 ms

  // Initialize PMSA003I
  // Ensure the PMSA003I is connected to the I2C bus (Wire)
  if (airSensor.begin(Wire)) {
    Serial.println("PMSA003I connected.");
  } else {
    Serial.println("Could not find PMSA003I sensor. Check connections!");
    display.clearDisplay(); display.setCursor(0, 0); display.println("PMSA003I Error!"); display.display();
    while (1);
  }

  // Initialize LTR-390UV-01
  if (!ltr.begin()) {
    Serial.println("Could not find LTR390 sensor. Check connections!");
    display.clearDisplay(); display.setCursor(0, 0); display.println("LTR390 Error!"); display.display();
    while (1);
  }
  ltr.setGain(LTR390_GAIN_3);
  ltr.setResolution(LTR390_RESOLUTION_18BIT);
  ltr.setMeasurementTime(LTR390_MEASUREMENTTIME_100MS);

  // Initialize rain sensor (analog pin)
  pinMode(RAIN_SENSOR_PIN, INPUT);

  // Initialize LoRaWAN
  Serial.println("Initializing LoRaWAN...");
  if (api.lorawan.init(LORAWAN_REGION) != true) {
    Serial.println("LoRaWAN init failed!");
    display.clearDisplay(); display.setCursor(0, 0); display.println("LoRaWAN Init Error!"); display.display();
    while (1);
  }

  api.lorawan.set_dev_eui(LORAWAN_DEVEUI);
  api.lorawan.set_app_eui(LORAWAN_APPEUI);
  api.lorawan.set_app_key(LORAWAN_APPKEY);
  api.lorawan.set_channel_mask(LORAWAN_CHANNEL_MASK);
  api.lorawan.set_preamble_length(LORAWAN_PREAMBLE_LENGTH);
  api.lorawan.set_adr_state(LORAWAN_ADR_STATE);
  api.lorawan.set_tx_power(LORAWAN_TX_POWER);
  api.lorawan.set_datarate(LORAWAN_DATARATE);
  api.lorawan.set_public_network_mode(LORAWAN_PUBLIC_NETWORK);
  api.lorawan.set_duty_cycle_mode(LORAWAN_DUTYCYCLE_ON); // Adjust for regional regulations

  // Join LoRaWAN network
  Serial.println("Attempting to join LoRaWAN network (OTAA)...");
  display.clearDisplay(); display.setCursor(0, 0); display.println("Joining LoRaWAN..."); display.display();
  if (api.lorawan.join()) {
    Serial.println("Joined LoRaWAN network successfully!");
    display.clearDisplay(); display.setCursor(0, 0); display.println("Joined LoRaWAN!"); display.display();
  } else {
    Serial.println("Failed to join LoRaWAN network. Retrying...");
    display.clearDisplay(); display.setCursor(0, 0); display.println("Join Failed!"); display.display();
    // In a real project, implement a retry mechanism with delay or low-power mode
    while (1);
  }
  delay(2000);
}

void loop() {
  readSensors();
  displayData();
  sendLoRaWANData();

  // Enter low-power mode to save battery
  Serial.println("Entering low-power mode...");
  api.system.sleep.all(60000); // Sleep for 60 seconds (60000 ms)
}

void readSensors() {
  Serial.println("Reading sensors...");

  // Read BME680
  if (!bme.performReading()) {
    Serial.println("Failed to read BME680.");
  } else {
    temperature = bme.temperature;
    humidity = bme.humidity;
    pressure = bme.pressure / 100.0; // hPa
    gas_resistance = bme.gas_resistance / 1000.0; // kOhms
    Serial.printf("Temp: %.2f C, Hum: %.2f %%, Pres: %.2f hPa, Gas: %.2f kOhms\n", temperature, humidity, pressure, gas_resistance);
  }

  // Read PMSA003I
  PMSA003I::pmsaData data;
  if (airSensor.read(&data)) {
    pm1_0 = data.PM1_0;
    pm2_5 = data.PM2_5;
    pm10_0 = data.PM10_0;
    Serial.printf("PM1.0: %d, PM2.5: %d, PM10.0: %d ug/m3\n", pm1_0, pm2_5, pm10_0);
  } else {
    Serial.println("Failed to read PMSA003I.");
  }

  // Read LTR-390UV-01
  if (ltr.newData()) {
    uv_index = ltr.readUVI();
    ambient_light = ltr.readALS();
    Serial.printf("UV Index: %.2f, Ambient Light: %.2f\n", uv_index, ambient_light);
  } else {
    Serial.println("Failed to read LTR390!");
  }

  // Read rain sensor
  int rain_analog_val = analogRead(RAIN_SENSOR_PIN);
  is_raining = (rain_analog_val < 500); // Simple threshold: if value is low, it's raining
  Serial.printf("Rain Sensor: %d (Raining: %s)\n", rain_analog_val, is_raining ? "Yes" : "No");
}

void displayData() {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.printf("Temp: %.1f C\n", temperature);
  display.printf("Hum: %.1f %%\n", humidity);
  display.printf("Pres: %.0f hPa\n", pressure);
  display.printf("PM2.5: %d ug/m3\n", pm2_5);
  display.printf("UV: %.1f\n", uv_index);
  display.printf("Rain: %s\n", is_raining ? "Yes" : "No");
  display.display();
}

void sendLoRaWANData() {
  // Create a buffer for the LoRaWAN payload
  // Using Cayenne LPP format for easy decoding in The Things Network
  // Ensure your Payload Formatter in TTN is set to "Cayenne LPP"
  uint8_t lpp_payload[50]; // Sufficient space for data
  uint8_t lpp_payload_size = 0;

  // Channel 1: Temperature (BME680)
  lpp_payload[lpp_payload_size++] = 1; // Channel
  lpp_payload[lpp_payload_size++] = LPP_TEMPERATURE;
  lpp_payload[lpp_payload_size++] = (uint8_t)(temperature * 2); // LPP_TEMPERATURE is 0.5 C/unit

  // Channel 2: Humidity (BME680)
  lpp_payload[lpp_payload_size++] = 2; // Channel
  lpp_payload[lpp_payload_size++] = LPP_HUMIDITY;
  lpp_payload[lpp_payload_size++] = (uint8_t)(humidity * 2); // LPP_HUMIDITY is 0.5 %/unit

  // Channel 3: Pressure (BME680)
  lpp_payload[lpp_payload_size++] = 3; // Channel
  lpp_payload[lpp_payload_size++] = LPP_BAROMETRIC_PRESSURE;
  // LPP_BAROMETRIC_PRESSURE is 2 hPa/unit, sent as 2 bytes
  lpp_payload[lpp_payload_size++] = (uint8_t)((uint16_t)(pressure / 2) >> 8);
  lpp_payload[lpp_payload_size++] = (uint8_t)((uint16_t)(pressure / 2) & 0xFF);

  // Channel 4: Gas Resistance (BME680) - Use LPP_ANALOG_INPUT for generic values (0.01 unit/unit)
  lpp_payload[lpp_payload_size++] = 4; // Channel
  lpp_payload[lpp_payload_size++] = LPP_ANALOG_INPUT;
  // Convert to int16 and scale by 100 for 2 decimal places
  int16_t gas_res_scaled = (int16_t)(gas_resistance * 100);
  lpp_payload[lpp_payload_size++] = (uint8_t)(gas_res_scaled >> 8);
  lpp_payload[lpp_payload_size++] = (uint8_t)(gas_res_scaled & 0xFF);

  // Channel 5: UV Index (LTR390) - Use LPP_ANALOG_INPUT (0.01 unit/unit)
  lpp_payload[lpp_payload_size++] = 5; // Channel
  lpp_payload[lpp_payload_size++] = LPP_ANALOG_INPUT;
  int16_t uv_scaled = (int16_t)(uv_index * 100);
  lpp_payload[lpp_payload_size++] = (uint8_t)(uv_scaled >> 8);
  lpp_payload[lpp_payload_size++] = (uint8_t)(uv_scaled & 0xFF);

  // Channel 6: PM2.5 (PMSA003I) - Use LPP_UNITS or LPP_ANALOG_INPUT for integers
  // Since PM2.5 is usually an integer, we can send it directly as 2 bytes
  lpp_payload[lpp_payload_size++] = 6; // Channel
  lpp_payload[lpp_payload_size++] = LPP_ANALOG_INPUT; // Using ANALOG_INPUT for generic integer
  lpp_payload[lpp_payload_size++] = (uint8_t)(pm2_5 >> 8);
  lpp_payload[lpp_payload_size++] = (uint8_t)(pm2_5 & 0xFF);

  // Channel 7: Rain Sensor (Analog Value) - Use LPP_ANALOG_INPUT
  lpp_payload[lpp_payload_size++] = 7; // Channel
  lpp_payload[lpp_payload_size++] = LPP_ANALOG_INPUT;
  lpp_payload[lpp_payload_size++] = (uint8_t)(rain_sensor_value >> 8);
  lpp_payload[lpp_payload_size++] = (uint8_t)(rain_sensor_value & 0xFF);

  // Channel 8: Is Raining (Digital value)
  lpp_payload[lpp_payload_size++] = 8; // Channel
  lpp_payload[lpp_payload_size++] = LPP_DIGITAL_INPUT;
  lpp_payload[lpp_payload_size++] = is_raining ? 1 : 0;

  Serial.print("Sending LoRaWAN packet (Cayenne LPP): ");
  for (int i = 0; i < lpp_payload_size; i++) {
    Serial.printf("%02X ", lpp_payload[i]);
  }
  Serial.println();

  // Send the LoRaWAN packet
  // Port LORAWAN_APP_PORT (1), confirmation (true for confirmed, false for unconfirmed)
  if (api.lorawan.send(lpp_payload, lpp_payload_size, LORAWAN_APP_PORT, true) == 0) {
    Serial.println("LoRaWAN packet sent successfully.");
    display.clearDisplay(); display.setCursor(0, 0); display.println("Packet Sent!"); display.display();
  } else {
    Serial.println("Failed to send LoRaWAN packet.");
    display.clearDisplay(); display.setCursor(0, 0); display.println("Send Failed!"); display.display();
  }
}

// Example Payload Formatter (Decoder) for The Things Network (Custom Javascript)
// If you are using Cayenne LPP, TTN will automatically decode it.
// If you choose to send a custom binary payload (as in the original provided code's comments),
// you would use a decoder like this:
/*
function decodeUplink(input) {
  var data = {};
  var bytes = input.bytes;

  // Assuming a custom payload structure (matches the original provided code's comment):
  // Byte 0-1: Temperature (x100)
  // Byte 2-3: Humidity (x100)
  // Byte 4-5: Pressure (x10)
  // Byte 6-7: PM2.5
  // Byte 8: UV Index (x10)
  // Byte 9: Rain (0 or 1)
  // NOTE: This custom payload is different from the Cayenne LPP payload above.
  // Make sure your Arduino code's sendLoRaWANData() function matches your decoder.

  // The custom payload in the original provided code's comment has 12 bytes but byte 0 and 1 are empty.
  // And the array indexing is off. I'll adjust the decoder based on the assumed intention.

  // Decoder for the provided C++ payload structure in the original prompt (adjusted for common practice):
  // Byte 0: UV Index (x10)
  // Byte 1: Rain (0 or 1)
  // Byte 2-3: Temperature (x100)
  // Byte 4-5: Humidity (x100)
  // Byte 6-7: Pressure (x10)
  // Byte 8-9: PM2.5 (uint16)

  // This decoder assumes the custom payload structure you provided in the original C++ comment.
  // Please ensure the `sendLoRaWANData()` function in your Arduino code actually sends data
  // in this exact format if you intend to use this custom decoder.
  // If you use the Cayenne LPP code provided above, you don't need this custom decoder.

  data.uv_index = (bytes[0] / 10.0);
  data.is_raining = (bytes[1] === 1);
  data.temperature = ((bytes[2] << 8 | bytes[3]) / 100.0);
  data.humidity = ((bytes[4] << 8 | bytes[5]) / 100.0);
  data.pressure = ((bytes[6] << 8 | bytes[7]) / 10.0);
  data.pm2_5 = (bytes[8] << 8 | bytes[9]);

  return {
    data: data,
    warnings: [],
    errors: []
  };
}
*/

Credits

Miguel Montiel Vega
11 projects • 9 followers
Teacher at Maude Studio & Erasmus+ project member: "Developing Solutions to Sustainability Using IoT" (2022-1-PT01-KA220-VET-000090202)
Thanks to Jose Migue fuentes.

Comments