LoRa weather station with acknowledgments on an Arduino Uno
A BME280 node sends temperature, humidity and pressure over LoRa with RadioHead, and a gateway acknowledges each reading and shows it on an I2C LCD. The whole build, an Arduino Uno and 4 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 60 minutes to build and run. Runs in your browser. Free, and no account needed.
Build it as the Weather Station Kit: the parts in numbered bags and a booklet that checks every step. Retails for about $103, free on Mokxi.
What you'll learn
- Sending sensor data over LoRa with RadioHead
- Acknowledgments and retries for reliable messages
- Packing values into a small fixed struct
- Reading a BME280 over I2C
- Why both radios must use the same frequency
- Difficulty: Advanced
- Time: About 60 minutes
- Board: Arduino Uno R3
- Parts: 8
How it works
This project is two Arduinos, each with an SX1278 LoRa radio. The node reads a BME280 for temperature, humidity and pressure every 5 seconds. It packs the three values and a counter into 8 bytes and sends them at 433 MHz.
The gateway listens. When a reading arrives, it sends an acknowledgment back, shows the values on a 16x2 LCD and prints them with the signal strength (RSSI) in the serial monitor.
If the node gets no acknowledgment, the RadioHead library resends up to 3 times. After that the node prints that no acknowledgment came. If the gateway hears nothing for 20 seconds, its LCD says node quiet.
Parts list
8 parts, plus the jumper wires. They retail for about $72; on Mokxi they're free.
- 2 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 2 × LoRa module, SX1278 (Ra-02)In Weather Station Kit.
- 1 × Pressure, humidity and temperature, BME280In Weather Station Kit.
- 2 × Power, 5 V
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| LoRa module, SX1278 (Ra-02) (1) pin GND | Arduino Uno R3 (1) pin GND |
| LoRa module, SX1278 (Ra-02) (1) pin SCK | Arduino Uno R3 (1) pin 13 |
| LoRa module, SX1278 (Ra-02) (1) pin MISO | Arduino Uno R3 (1) pin 12 |
| LoRa module, SX1278 (Ra-02) (1) pin MOSI | Arduino Uno R3 (1) pin 11 |
| LoRa module, SX1278 (Ra-02) (1) pin NSS | Arduino Uno R3 (1) pin 10 |
| LoRa module, SX1278 (Ra-02) (1) pin RST | Arduino Uno R3 (1) pin 9 |
| LoRa module, SX1278 (Ra-02) (1) pin DIO0 | Arduino Uno R3 (1) pin 2 |
| LoRa module, SX1278 (Ra-02) (1) pin 3.3V | Arduino Uno R3 (1) pin 3V3 |
| Pressure, humidity and temperature, BME280 pin SDA | Arduino Uno R3 (1) pin A4 |
| Pressure, humidity and temperature, BME280 pin SCL | Arduino Uno R3 (1) pin A5 |
| LoRa module, SX1278 (Ra-02) (2) pin GND | Arduino Uno R3 (2) pin GND |
| LoRa module, SX1278 (Ra-02) (2) pin SCK | Arduino Uno R3 (2) pin 13 |
| LoRa module, SX1278 (Ra-02) (2) pin MISO | Arduino Uno R3 (2) pin 12 |
| LoRa module, SX1278 (Ra-02) (2) pin MOSI | Arduino Uno R3 (2) pin 11 |
| LoRa module, SX1278 (Ra-02) (2) pin NSS | Arduino Uno R3 (2) pin 10 |
| LoRa module, SX1278 (Ra-02) (2) pin RST | Arduino Uno R3 (2) pin 9 |
| LoRa module, SX1278 (Ra-02) (2) pin DIO0 | Arduino Uno R3 (2) pin 2 |
| LoRa module, SX1278 (Ra-02) (2) pin 3.3V | Arduino Uno R3 (2) pin 3V3 |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 (2) pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 (2) pin A5 |
| Pressure, humidity and temperature, BME280 pin VIN | 5 V |
| Pressure, humidity and temperature, BME280 pin GND | Ground |
| Character LCD with I2C backpack pin GND | Ground |
| Character LCD with I2C backpack pin VCC | 5 V |
The note on the canvas
Word for word, as it sits beside the circuit in the editor.
Try it
1. Press Run. Every 5 seconds the node sends its BME280 reading, and the gateway's LCD shows it.
2. Drag the BME280's sliders: the next reading on the LCD follows.
3. The node's serial monitor says "acknowledged" for every reading the gateway got.
4. Hold the gateway's reset button: the node retries 3 times, then says no acknowledgment came. Let go and it is acknowledged again.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// LoRa weather station, the sensor node: a BME280 read every 5 seconds and
// sent over an SX1278 (Ra-02) LoRa module to the gateway with RadioHead's
// RHReliableDatagram. The gateway acknowledges every reading; with no
// acknowledgment the library sends again, up to 3 retries, and the sketch
// says so if all of them go unanswered.
//
// Ra-02 NSS -> 10 DIO0 -> 2 RST -> 9 SCK -> 13 MOSI -> 11
// MISO -> 12 3.3V -> 3.3V GND -> GND
// BME280 SDA -> A4 SCL -> A5 VIN -> 5V GND -> GND
//
// The SX1278 runs on 3.3 V and its pins are not 5 V tolerant: on the bench,
// put a level shifter on NSS, SCK, MOSI and RST, or use a 3.3 V board.
//
// Things to try. Drag the BME280's sliders. Hold the gateway's reset button
// and watch the retries run out. Change INTERVAL_MS for slower reports,
// which a battery node would use.
#include <SPI.h>
#include <RH_RF95.h>
#include <RHReliableDatagram.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
const uint8_t NODE_ADDRESS = 1;
const uint8_t GATEWAY_ADDRESS = 2;
const float FREQUENCY = 433.0; // MHz: the Ra-02 is a 433 MHz module
const unsigned long INTERVAL_MS = 5000;
const uint8_t RESET_PIN = 9;
RH_RF95 rf95(10, 2); // NSS, DIO0
RHReliableDatagram manager(rf95, NODE_ADDRESS);
Adafruit_BME280 bme;
// What goes on the air: 8 bytes, the same layout on both boards.
struct __attribute__((packed)) Reading {
int16_t centiC; // temperature, hundredths of a degree C
uint16_t deciHum; // humidity, tenths of a percent
uint16_t deciHpa; // pressure, tenths of a hectopascal
uint16_t count;
};
uint16_t count = 0;
unsigned long lastSent = 0;
void setup() {
Serial.begin(9600);
// Reset the radio, as the module's datasheet asks, before talking to it.
pinMode(RESET_PIN, OUTPUT);
digitalWrite(RESET_PIN, LOW);
delay(10);
digitalWrite(RESET_PIN, HIGH);
delay(10);
if (!bme.begin(0x76)) {
Serial.println("no BME280 at 0x76: check SDA and SCL");
while (1) {}
}
if (!manager.init()) {
Serial.println("LoRa radio not found: check NSS, DIO0 and the 3.3 V supply");
while (1) {}
}
rf95.setFrequency(FREQUENCY);
rf95.setTxPower(13);
manager.setRetries(3);
manager.setTimeout(500); // ms to wait for an acknowledgment
Serial.println("LoRa weather node 1 ready");
}
void loop() {
if (millis() - lastSent < INTERVAL_MS && lastSent != 0) return;
lastSent = millis();
float t = bme.readTemperature();
float h = bme.readHumidity();
float p = bme.readPressure() / 100.0;
Reading r = {(int16_t)lround(t * 100), (uint16_t)lround(h * 10), (uint16_t)lround(p * 10), ++count};
Serial.print("reading ");
Serial.print(count);
Serial.print(": ");
Serial.print(t, 1);
Serial.print(" C, ");
Serial.print(h, 0);
Serial.print(" %, ");
Serial.print(p, 1);
Serial.print(" hPa: ");
if (manager.sendtoWait((uint8_t *)&r, sizeof(r), GATEWAY_ADDRESS)) {
Serial.println("acknowledged");
} else {
Serial.println("no acknowledgment after 3 retries");
}
}
// LoRa weather station, the gateway: receives the sensor node's readings
// over an SX1278 (Ra-02) with RadioHead's RHReliableDatagram, acknowledges
// each one, and shows the latest on a 16x2 I2C LCD. Every reading also goes
// to the serial monitor with its RSSI, ready for a computer to log.
//
// Ra-02 NSS -> 10 DIO0 -> 2 RST -> 9 SCK -> 13 MOSI -> 11
// MISO -> 12 3.3V -> 3.3V GND -> GND
// LCD SDA -> A4 SCL -> A5 VCC -> 5V GND -> GND
//
// The SX1278 runs on 3.3 V and its pins are not 5 V tolerant: on the bench,
// put a level shifter on NSS, SCK, MOSI and RST, or use a 3.3 V board.
//
// Things to try. Change FREQUENCY here only and nothing arrives. Change
// STALE_MS to flag a quiet node sooner.
#include <SPI.h>
#include <RH_RF95.h>
#include <RHReliableDatagram.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
const uint8_t GATEWAY_ADDRESS = 2;
const float FREQUENCY = 433.0; // MHz, the same as the node
const unsigned long STALE_MS = 20000;
const uint8_t RESET_PIN = 9;
RH_RF95 rf95(10, 2); // NSS, DIO0
RHReliableDatagram manager(rf95, GATEWAY_ADDRESS);
LiquidCrystal_I2C lcd(0x27, 16, 2);
struct __attribute__((packed)) Reading {
int16_t centiC;
uint16_t deciHum;
uint16_t deciHpa;
uint16_t count;
};
uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
unsigned long lastHeard = 0;
bool heard = false;
bool stale = false;
void setup() {
Serial.begin(9600);
lcd.init();
lcd.backlight();
pinMode(RESET_PIN, OUTPUT);
digitalWrite(RESET_PIN, LOW);
delay(10);
digitalWrite(RESET_PIN, HIGH);
delay(10);
if (!manager.init()) {
Serial.println("LoRa radio not found: check NSS, DIO0 and the 3.3 V supply");
lcd.print("No LoRa radio");
while (1) {}
}
rf95.setFrequency(FREQUENCY);
rf95.setTxPower(13);
Serial.println("LoRa gateway listening");
lcd.print("Waiting for node");
}
void loop() {
uint8_t len = sizeof(buf);
uint8_t from;
// recvfromAck() sends the acknowledgment before it returns.
if (manager.recvfromAck(buf, &len, &from)) {
if (len != sizeof(Reading)) return;
Reading r;
memcpy(&r, buf, sizeof(r));
heard = true;
stale = false;
lastHeard = millis();
lcd.clear();
lcd.print(r.centiC / 100.0, 1);
lcd.print(" C ");
lcd.print(r.deciHum / 10.0, 0);
lcd.print(" %");
lcd.setCursor(0, 1);
lcd.print(r.deciHpa / 10.0, 1);
lcd.print(" hPa");
Serial.print("node ");
Serial.print(from);
Serial.print(" #");
Serial.print(r.count);
Serial.print(": ");
Serial.print(r.centiC / 100.0, 1);
Serial.print(" C, ");
Serial.print(r.deciHum / 10.0, 0);
Serial.print(" %, ");
Serial.print(r.deciHpa / 10.0, 1);
Serial.print(" hPa, RSSI ");
Serial.println(rf95.lastRssi());
}
if (heard && !stale && millis() - lastHeard > STALE_MS) {
stale = true;
lcd.setCursor(0, 1);
lcd.print("node quiet 20 s ");
Serial.println("no reading for 20 s");
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
On both boards it pulses the radio's reset pin 9, then starts the radio with manager.init. The node also starts the BME280 at address 0x76. Both set 433 MHz and 13 dBm power. The node sets 3 retries and a 500 ms wait for each acknowledgment. If a part is missing, the sketch prints what to check and stops.
void setup() {
Serial.begin(9600);
// Reset the radio, as the module's datasheet asks, before talking to it.
pinMode(RESET_PIN, OUTPUT);
digitalWrite(RESET_PIN, LOW);
delay(10);
digitalWrite(RESET_PIN, HIGH);
delay(10);
if (!bme.begin(0x76)) {
Serial.println("no BME280 at 0x76: check SDA and SCL");
while (1) {}
}
if (!manager.init()) {
Serial.println("LoRa radio not found: check NSS, DIO0 and the 3.3 V supply");
while (1) {}
}
rf95.setFrequency(FREQUENCY);
rf95.setTxPower(13);
manager.setRetries(3);
manager.setTimeout(500); // ms to wait for an acknowledgment
Serial.println("LoRa weather node 1 ready");
}loop sketch.ino
The node waits for INTERVAL_MS, reads the BME280, rounds the values into whole numbers and sends them with sendtoWait, which returns true only when the gateway acknowledges. The gateway calls recvfromAck, which sends the acknowledgment itself, then shows the reading and watches for STALE_MS.
void loop() {
if (millis() - lastSent < INTERVAL_MS && lastSent != 0) return;
lastSent = millis();
float t = bme.readTemperature();
float h = bme.readHumidity();
float p = bme.readPressure() / 100.0;
Reading r = {(int16_t)lround(t * 100), (uint16_t)lround(h * 10), (uint16_t)lround(p * 10), ++count};
Serial.print("reading ");
Serial.print(count);
Serial.print(": ");
Serial.print(t, 1);
Serial.print(" C, ");
Serial.print(h, 0);
Serial.print(" %, ");
Serial.print(p, 1);
Serial.print(" hPa: ");
if (manager.sendtoWait((uint8_t *)&r, sizeof(r), GATEWAY_ADDRESS)) {
Serial.println("acknowledged");
} else {
Serial.println("no acknowledgment after 3 retries");
}
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- Nothing arrives at the gateway. This project
Both sketches must use the sameFREQUENCY. Changing it on only one board breaks the link. - The monitor says LoRa radio not found. This project
Check NSS on pin 10, DIO0 on pin 2, RST on pin 9, and the 3.3 V supply to the radio. - The monitor says no BME280 at 0x76. This project
Check the SDA and SCL wires on A4 and A5 and the sensor power. - The module resets or the radio does not start. LoRa module
It is a 3.3 V part. Power it at 3.3 V, with a decent supply that can give over 100 mA when transmitting, and add a 10 uF or more capacitor. Never connect 5 V to VCC or to the logic pins. - The two modules never hear each other. LoRa module
Both must use the same frequency, such as 433 MHz, plus the same spreading factor, bandwidth and sync word. Attach the antenna before transmitting. Check SPI wiring: SCK, MISO, MOSI, NSS, RST and DIO0. - The sketch cannot find the sensor, or reads zeros. Pressure
The I2C address is 0x76 when SDO is tied to GND and 0x77 when tied to VCC. Run an I2C scanner, and make sure the library address matches. Wire SDA and SCL correctly. - Humidity always reads zero or looks wrong. Pressure
A BMP280 looks the same but has no humidity sensor. Check the chip ID register: 0x60 is a BME280 and 0x58 is a BMP280. Use the right library for yours. - The screen stays blank or shows only a row of blocks. Character LCD with I2C backpack
The I2C address is usually 0x27 or 0x3F. Run an I2C scanner to find yours. Then turn the contrast trimmer on the backpack slowly until text appears.
Questions and answers
What does RHReliableDatagram add over plain LoRa?
An acknowledgment for every message. The gateway's recvfromAck() replies before it returns; if no reply comes within the timeout (500 ms here), sendtoWait() sends again, up to 3 retries, and then returns false.
What is sent over the air?
An 8-byte packed struct with the same layout on both boards: the temperature in hundredths of a degree, humidity and pressure in tenths, and a count.
Which frequency should I use?
The Ra-02 is a 433 MHz module, so both sketches call setFrequency(433.0). Use a frequency that is legal where you live, and the same one at both ends, or nothing arrives.
Can I wire the Ra-02 straight to a 5 V Uno?
It runs on 3.3 V and its pins are not 5 V tolerant. Put a level shifter on NSS, SCK, MOSI and RST, or use a 3.3 V board.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.