In this tutorial you will learn how to send environmental data with LoRa. LoRa allows you to transmit sensor data over long distances (several kilometers) while consuming very little power. We will use this to transmit Temperature, Humidity and Air Pressure data measured by an BME280 between two ESP32’s connected to SX1276 LoRa modules.
If you not familiar with LoRa, I suggest you read our Long range communication with LoRa SX1276 and ESP32 tutorial first, since it provides you with the background information needed for this tutorial.
Required Parts
Below you will find the required components. I used an older ESP32 board, which has been deprecated but you can still get it for a very low price. However, any other ESP32 will work just fine as well. Just remember you will need two of them and two SX1276 modules – one for sending and one for receiving.
As for the SX1276 LoRa transceiver modules watch out what version you buy! Depending on the country the permitted frequencies are different (link). It is 868MHz in Europe, 915MHz in North America and 433MHz in Asia.
The module description either lists the frequency or has a number such as 868 or 915 in the name. I listed a module with the 868MHz, since I am in Europe. But you can get this module also for the 915MHz band.
For the environmental sensor, I picked the BME280, since it comes with a small form factor, has a sleep mode and can measure Temperature, Humidity, Air Pressure and Altitude. Note that there are 5V and 3.3V versions of the BME280 breakout board. Make sure to buy the 3.3V version! If you need only Temperature and Humidity, the Si7021 is a good alternative.

2 x Lora 868/915M Module SX1276

2 x ESP32 lite

BME280 Sensor

USB Data Cable

Dupont Wire Set

2x Breadboard
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
SX1276 LoRa Module
The SX1276 is a LoRa transceiver made by Semtech. Most modules (breakout boards) based on this IC are configured for either 433 MHz (Asia), 868 MHz (Europe), or 915 MHz (North America), depending on your region. On the back of module you usually find a small table where the operating frequency of the specific module is marked. The picture below shows a module that operates in the 915 MHz band and therefore could be used in North America:

Make sure you use a module with the correct transmission frequency for your country! The module can communicate over distances of 2 to 15+ kilometers, depending on antenna quality, terrain, and settings. The SX1276 supports data rates from 0.018 kbps to 37.5 kbps. For more technical details see the datasheet linked below:
The module has 16 pins. One for the Antenna (ANT), one for power (VCC), several ground pins (GND), the pins for SPI (REST, NSS, SCK, MOSI, MISO) and six configurable Digital IO pins (DIO).
Note that the SX1276 uses 3.3V logic and you therefore cannot connect it directly to an Arduino UNO, which operates a 5V logic levels. If you want to use an Arduino instead of an ESP32 you either must use a logic level shifter or an Arduino board that uses 3.3V logic.
BME280 Sensor
For measuring environmental data such as temperature, humidity and air pressure we are going to use the BME280 sensor.
One reason is that it has a low power, sleep mode, where it consumes only 0.1µA (3.6 μA in normal mode). In combination with a LoRa module and the ESP32-lite with a deep-sleep current of 5.65mA (at 5V), this makes for a very low-power setup that can run a long time on battery power.
The BME280 sensor itself is tiny and typically comes on a breakout board with an I2C interface; see the picture below.

The sensor can measure pressure from 300 hPa to 1100 hPa, temperature from -40°C to +85°C, and humidity from 0% to 100%. For more information on the BME280 read our tutorial on How To Use BME280 Pressure Sensor With Arduino or the Weather Station on e-Paper Display tutorial.
Connecting SX1276 to ESP32
The SX1276 is controlled via an SPI interface. When wiring it to an ESP32 we will have to make the following connections:
| SX1276 | ESP32 |
|---|---|
| MOSI | 23 |
| MSIO | 19 |
| SCK | 18 |
| RST | 17 |
| NSS | 5 |
| DIO0 | 4 |
| GND | GND |
| VCC | 3.3V |
Make sure that you connect VCC to the 3.3V output pin of your ESP32. We don’t really use DIO0, so you could leave that connection out. DIO0 is needed if you want to add an interrupt handler that get’s called when a measurement is complete. The picture below shows the complete wiring diagram (including DIO0):

If you are using a different ESP32 board, the pins for hardware SPI, which the above wiring is relying on, may differ. If you are not sure which pins of your board to use have a look at the Find I2C and SPI default pins tutorial.
The photo below shows the wiring of the SX1276 LoRa Receiver with the ESP32 on a breadboard:

Note that the SX1276 module has a pin hole spacing of 2mm and does not directly fit a breadboard. See the Long range communication with LoRa SX1276 and ESP32 tutorial on how to build a breadboard-friendly version of the module.
Connecting BME280 to ESP32
We want to measure ambient temperature, humidity and air pressure using the BME280 sensor. Since hardware I2C/SPI pins of the ESP32 are already connected to SX1276, we use software I2C with the free pins 33 for SDA and pin 25 for SCL as show below.

Below is the connection table for the BME280 and the ESP32. You can use different pins for I2C but if you do, don’t forget to adjust the code in the next section accordingly.
| BME280 | ESP32 |
|---|---|
| SDA | 33 |
| SCL | 25 |
| GND | GND |
| VCC | 3.3V |
Note that there are 5V and 3.3V versions of the BME280 breakout board. I am using the 3.3V version and therefore connect VCC to 3.3V pin of the ESP32.
Since the WEMOS LOLIN32 Lite has a built in battery port and charger, you can power the entire system from a LiPo battery. The picture below shows the complete wiring with a LiPo battery attached:

I measured a deep sleep current of 0.2mA and a current of 140mA when transmitting data. If you use a 18650 battery, which has a capacity of around 3300mA (depending on the specific brand), you probably can run the sensor for about a year if you measure and transmit data every minute.
Code for LoRa Receiver
In this section we write the code for the receiver side first, since it is simpler. The following code receives the temperature, humidity, and pressure from the BME280 sensor sent over the SX1276 LoRa module and displays the measurement on the Serial Monitor:
#include <SPI.h>
#include <LoRa.h>
// WEMOS LOLIN32 Lite
// MOSI -> 23
// MISO -> 19
// SCK -> 18
#define SS 5
#define RST 17
#define DIO0 4
void initLoRa() {
LoRa.setPins(SS, RST, DIO0);
// 433E6: Asia, 868E6: Europe, 915E6: North America
while (!LoRa.begin(868E6)) {
Serial.println(".");
delay(500);
}
LoRa.setSyncWord(0xF3); // 0-0xFF sync word to match the receiver
LoRa.setSpreadingFactor(12); // (6-12) higher value increases range but decreases data rate
LoRa.setSignalBandwidth(125E3); // lower value increases range but decreases data rate
LoRa.setCodingRate4(8); // higher value increases range but decreases data rate
LoRa.enableCrc();
}
void readLoRaData() {
static char data[128];
int i = 0;
while ((i < 128) && LoRa.available()) {
data[i++] = LoRa.read();
}
data[i] = '\0';
//Serial.println(data);
float temp, hum, pres;
sscanf(data, "%f|%f|%f", &temp, &hum, &pres);
Serial.printf("%.1fC %.0f%% %.0fhPa (%d)\n",
temp, hum, pres, LoRa.packetRssi());
}
void setup() {
Serial.begin(115200);
initLoRa();
}
void loop() {
if (LoRa.parsePacket()) {
readLoRaData();
}
}
Libraries
To start, the code includes the necessary libraries. The SPI.h library handles communication between the ESP32 and the LoRa module, while LoRa.h gives us an easy way to interface with the SX1276 module.
#include <SPI.h> #include <LoRa.h>
You can install the LoRa library by Sandeep Mistry using the Library Manager in the Arduino IDE. The picture below shows a successful installation of the library:

Constants
Next, the code defines the SPI pins specific to the WEMOS LOLIN32 Lite, which is a compact ESP32 development board. The SS, RST, and DIO0 pins are critical for LoRa communication and must be set correctly to match your wiring.
#define SS 5 #define RST 17 #define DIO0 4
initLoRa
Now comes the initLoRa() function. This function initializes the LoRa radio and configures it for your region and performance needs. First, it sets the control pins using LoRa.setPins().
LoRa.setPins(SS, RST, DIO0);
After that, the code attempts to start the LoRa radio at a frequency of 868 MHz, which is used in Europe. If the initialization fails, it retries every 500 milliseconds while printing dots to the Serial Monitor.
while (!LoRa.begin(868E6)) {
Serial.println(".");
delay(500);
}
Once the LoRa module starts, several parameters are configured to maximize range. The sync word is set to 0xF3, which helps filter out packets that aren’t from your own network. The spreading factor is increased to 12 for longer range, and the signal bandwidth is narrowed to 125 kHz to further boost range. The coding rate is set to 4/8, again prioritizing range over speed. Finally, CRC checking is enabled to ensure data integrity.
LoRa.setSyncWord(0xF3); LoRa.setSpreadingFactor(12); LoRa.setSignalBandwidth(125E3); LoRa.setCodingRate4(8); LoRa.enableCrc();
readLoRaData
The readLoRaData() function kicks in whenever a packet arrives. It reads the available bytes from the LoRa module into a character buffer, ensuring it doesn’t exceed 128 characters. Once done, it terminates the string with a null character.
static char data[128];
int i = 0;
while ((i < 128) && LoRa.available()) {
data[i++] = LoRa.read();
}
data[i] = '\0';
Next, it uses sscanf() to extract three floating-point values—temperature, humidity, and pressure—using the pipe (|) symbol as a delimiter. These values are then printed to the Serial Monitor in a readable format, along with the RSSI (Received Signal Strength Indicator), which tells you how strong the received signal is.
float temp, hum, pres;
sscanf(data, "%f|%f|%f", &temp, &hum, &pres);
Serial.printf("%.1fC %.0f%% %.0fhPa (%d)\n",
temp, hum, pres, LoRa.packetRssi());
setup
The setup() function is short and sweet. It initializes the Serial interface at 115200 baud for debugging and calls initLoRa() to get the radio ready.
void setup() {
Serial.begin(115200);
initLoRa();
}
loop
Finally, the loop() function checks for incoming LoRa packets continuously. When it detects a packet using LoRa.parsePacket(), it calls readLoRaData() to process the information.
void loop() {
if (LoRa.parsePacket()) {
readLoRaData();
}
}
Instead of printing to the Serial Monitor you could display the environmental data on an OLED or e-Paper display, log to an SD card, or upload to the cloud.
Code for LoRa Sender
This section contains the code for the sender. The code complements the receiver describe above. The sender measures environmental data such as temperature, humidity and air pressure, transmits the data every minute via LoRa and switches the ESP32 in deep-sleep mode between transmissions to preserve battery power:
#include <Wire.h>
#include <SPI.h>
#include <LoRa.h>
#include <Adafruit_BME280.h>
// WEMOS LOLIN32 Lite
// MOSI -> 23
// MISO -> 19
// SCK -> 18
#define SS 5
#define RST 17
#define DIO0 4
Adafruit_BME280 bme;
void initBMESensor() {
Wire.begin(33, 25); // Software I2C for BME280
bme.begin(0x76, &Wire);
bme.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperature
Adafruit_BME280::SAMPLING_X1, // pressure
Adafruit_BME280::SAMPLING_X1, // humidity
Adafruit_BME280::FILTER_OFF);
}
void initLoRa() {
LoRa.setPins(SS, RST, DIO0);
// 433E6: Asia, 868E6: Europe, 915E6: North America
while (!LoRa.begin(868E6)) {
Serial.println(".");
delay(500);
}
LoRa.setSyncWord(0xF3); // 0-0xFF sync word to match the receiver
LoRa.setSpreadingFactor(12); // (6-12) higher value increases range but decreases data rate
LoRa.setSignalBandwidth(125E3); // lower value increases range but decreases data rate
LoRa.setCodingRate4(8); // higher value increases range but decreases data rate
LoRa.enableCrc();
}
void sendLoRaData() {
static char data[128];
bme.takeForcedMeasurement();
float temp = bme.readTemperature();
float hum = bme.readHumidity();
float pres = bme.readPressure() / 100.0;
sprintf(data, "%.2f|%.2f|%.2f", temp, hum, pres);
Serial.print(data);
LoRa.beginPacket();
LoRa.print(data);
LoRa.endPacket();
}
void shutdownPeripherals() {
LoRa.sleep();
Wire.end();
pinMode(33, INPUT);
pinMode(25, INPUT);
SPI.end();
pinMode(23, INPUT); // MOSI
pinMode(19, INPUT); // MISO
pinMode(18, INPUT); // SCK
pinMode(SS, INPUT); // SS
pinMode(RST, INPUT); // RST
pinMode(DIO0, INPUT); // DIO0
}
void setup() {
Serial.begin(115200);
initBMESensor();
initLoRa();
sendLoRaData();
shutdownPeripherals();
esp_sleep_enable_timer_wakeup(60 * 1000 * 1000); // 1 min
esp_deep_sleep_start();
}
void loop() {
}
Libraries
First, the code includes libraries for both LoRa communication and BME280 sensor access. While we’ve already discussed SPI.h and LoRa.h, this sketch also uses Wire.h for I2C communication and Adafruit_BME280.h, which provides a high-level interface for the sensor.
#include <Wire.h> #include <SPI.h> #include <LoRa.h> #include <Adafruit_BME280.h>
You can install the Adafruit_BME280 library via Library Manager and after a successful installation it should look like this:

Objects
The BME280 sensor is instantiated using Adafruit’s driver. This object, bme, will handle all readings.
Adafruit_BME280 bme;
initBMESensor
The initBMESensor() function initializes the BME280 using software I2C. Instead of the ESP32’s default I2C pins, it specifies GPIO 33 for SDA and GPIO 25 for SCL, which can be useful when the default I2C bus is in use elsewhere.
Wire.begin(33, 25); // Software I2C for BME280 bme.begin(0x76, &Wire);
Note that I am using the I2C address 0x76, when configuring the sensor via bme.begin(0x76, &Wire). Your sensor may have a different I2C address. For instance, 0x77 is also common for the BME280.
The sensor is configured in forced mode, meaning it only measures when explicitly told to do so. This approach saves power. Sampling is set to the minimum (1x) for temperature, pressure, and humidity, and the onboard filter is disabled.
bme.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::FILTER_OFF);
initLora
The initLoRa() function is identical to the receiver sketch and sets up the LoRa module for long-range, low-bitrate communication. If you need a refresher on how that works, scroll up to the previous explanation.
sendLoRaData
Now the heart of the sketch: sendLoRaData(). This function reads the latest environmental values from the BME280 and sends them over LoRa. First, it forces the sensor to take a new measurement.
bme.takeForcedMeasurement();
It then reads temperature, humidity, and pressure values. Pressure is divided by 100 to convert from Pascals to hPa, matching the format expected by the receiver.
float temp = bme.readTemperature(); float hum = bme.readHumidity(); float pres = bme.readPressure() / 100.0;
The sensor data is then formatted into a single string, using the | character as a delimiter. This matches the expected format on the receiver side:
sprintf(data, "%.2f|%.2f|%.2f", temp, hum, pres); Serial.print(data);
A typical string looks like this: 25.59|54.93|1001.23. Instead of sending a string we could also send the data as bytes. Each float has 4 bytes and sending 3×4 = 16 bytes, would reduce the amount of data to send. On the other hand, adding more data and unpacking the data on the receiver side would be more complex.
Next, the code begins a LoRa packet, writes the data, and closes the packet to transmit it.
LoRa.beginPacket(); LoRa.print(data); LoRa.endPacket();
shutdownPeripherals
Once the data is sent, the shutdownPeripherals() function powers down everything that’s not needed. It puts the LoRa module to sleep, ends the I2C and SPI buses, and reconfigures all associated GPIO pins as inputs to prevent unnecessary power draw.
LoRa.sleep(); Wire.end(); pinMode(33, INPUT); pinMode(25, INPUT); SPI.end(); pinMode(23, INPUT); pinMode(19, INPUT); pinMode(18, INPUT); pinMode(SS, INPUT); pinMode(RST, INPUT); pinMode(DIO0, INPUT);
setup
The setup() function ties everything together. It initializes the Serial interface, BME280 sensor, and LoRa module. Then it sends the data and shuts everything down. Finally, it configures the ESP32 to wake up from deep sleep after 60 seconds using a timer wakeup, and enters deep sleep mode immediately.
esp_sleep_enable_timer_wakeup(60 * 1000 * 1000); // 1 min esp_deep_sleep_start();
loop
The loop() remains empty because the ESP32 never reaches it. After sending data, the microcontroller sleeps until it wakes up to repeat the cycle.
void loop() {
}
And that’s it. With the code for the sender and receiver you have a system to measure environmental data with sensor that is kilometers away and can run for a long time on battery power.
Conclusions
In this tutorial you learned how to measure environmental data such as temperature, humidity and air pressure with a BME280 sensor and how to transmit this data over long distances using LoRa and the SX1276 transceiver module.
In contrast to WiFi or Bluetooth, LoRa allows you to send data from sensors that are kilometers away and need to run on battery power for a long time. I didn’t spend much time minimizing the battery consumption and the following BME280 Sleep Mode video may have additional information that is useful.
If you transmit data only between a few sensors raw LoRa is fine. But if you have many sensors you need to manage and want to send measurements to the internet LoRaWAN is a better choice. See the LoRaWAN with Thinknode G1 Gateway tutorial for more information on this topic.
We printed the measurements to the Serial Monitor but you could easily extend the code to show the data on a display. Have a look at the Weather Station on e-Paper Display and the How to configure TFT_eSPI Library for TFT display tutorials.
If you want to also show weather data from the internet or the current time, I suggest the Simple ESP32 Internet Weather Station and the Automatic Daylight Savings Time Clock tutorials.
If you have any questions feel free to leave them in the comment section.
Happy Tinkering ; )
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.

