In this project, you’ll build a sensor monitoring system using a TTGO LoRa32 SX1276 OLED board that sends temperature, humidity and pressure readings via LoRa radio to an ESP32 LoRa receiver. The receiver displays the latest sensor readings on a web server.

With this project you’ll learn how to:
Recommended reading: TTGO LoRa32 SX1276 OLED Board: Getting Started with Arduino IDE
Watch the video demonstration to see what you’re going to build throughout this tutorial.
The following image shows a high-level overview of the project we’ll build throughout this tutorial.

For an introduction to LoRa communication: what’s LoRa, LoRa frequencies, LoRa applications and more, read our Getting Started ESP32 with LoRa using Arduino IDE.
For this project, we’ll use the following components:
You’ll also need some jumper wires and a breadboard.
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
To program the TTGO LoRa32 SX1276 OLED boards we’ll use Arduino IDE. To upload files to the ESP32 filesystem, we’ll use the ESP32 LittleFS filesystem uploader plugin.
So, before proceeding, you need to install the ESP32 boards and the ESP32 filesystem uploader plugin in your Arduino IDE.
For this project, you need to install several libraries.
The following libraries can be installed through the Arduino Library Manager. Go to Sketch > Include Library> Manage Libraries and search for the libraries’ name.
Everytime the LoRa receiver picks up a new a LoRa message, it will request the date and time from an NTP server so that we know when the last packet was received.
For that we’ll be using the NTPClient library forked by Taranais. Follow the next steps to install this library in your Arduino IDE:
IMPORTANT: we’re not using the default NTPClient library. To follow this tutorial you need to install the library we recommend using the following steps.
The LoRa Sender is connected to a BME280 sensor and sends temperature, humidity, and pressure readings every 10 seconds. You can change this period of time later in the code.
Recommended reading: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity)
The BME280 we’re using communicates with the ESP32 using I2C communication protocol. Wire the sensor as shown in the next schematic diagram:

| BME280 | ESP32 |
| VIN | 3.3 V |
| GND | GND |
| SCL | GPIO 13 |
| SDA | GPIO 21 |
The following code reads temperature, humidity and pressure from the BME280 sensor and sends the readings via LoRa radio.
Copy the following code to your Arduino IDE.
/********* Rui Santos & Sara Santos - Random Nerd Tutorials Complete project details at https://RandomNerdTutorials.com/esp32-lora-sensor-web-server/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ //Libraries for LoRa #include <SPI.h> #include <LoRa.h> //Libraries for OLED Display #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> //Libraries for BME280 #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> //define the pins used by the LoRa transceiver module #define SCK 5 #define MISO 19 #define MOSI 27 #define SS 18 #define RST 14 #define DIO0 26 //433E6 for Asia //866E6 for Europe //915E6 for North America #define BAND 866E6 //OLED pins #define OLED_SDA 4 #define OLED_SCL 15 #define OLED_RST 16 #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels //BME280 definition #define SDA 21 #define SCL 13 TwoWire I2Cone = TwoWire(1); Adafruit_BME280 bme; //packet counter int readingID = 0; int counter = 0; String LoRaMessage = ""; float temperature = 0; float humidity = 0; float pressure = 0; Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST); //Initialize OLED display void startOLED(){ //reset OLED display via software pinMode(OLED_RST, OUTPUT); digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH); //initialize OLED Wire.begin(OLED_SDA, OLED_SCL); if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32 Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } display.clearDisplay(); display.setTextColor(WHITE); display.setTextSize(1); display.setCursor(0,0); display.print("LORA SENDER"); } //Initialize LoRa module void startLoRA(){ //SPI LoRa pins SPI.begin(SCK, MISO, MOSI, SS); //setup LoRa transceiver module LoRa.setPins(SS, RST, DIO0); while (!LoRa.begin(BAND) && counter < 10) { Serial.print("."); counter++; delay(500); } if (counter == 10) { // Increment readingID on every new reading readingID++; Serial.println("Starting LoRa failed!"); } Serial.println("LoRa Initialization OK!"); display.setCursor(0,10); display.clearDisplay(); display.print("LoRa Initializing OK!"); display.display(); delay(2000); } void startBME(){ I2Cone.begin(SDA, SCL, 100000); bool status1 = bme.begin(0x76, &I2Cone); if (!status1) { Serial.println("Could not find a valid BME280_1 sensor, check wiring!"); while (1); } } void getReadings(){ temperature = bme.readTemperature(); humidity = bme.readHumidity(); pressure = bme.readPressure() / 100.0F; } void sendReadings() { LoRaMessage = String(readingID) + "/" + String(temperature) + "&" + String(humidity) + "#" + String(pressure); //Send LoRa packet to receiver LoRa.beginPacket(); LoRa.print(LoRaMessage); LoRa.endPacket(); display.clearDisplay(); display.setCursor(0,0); display.setTextSize(1); display.print("LoRa packet sent!"); display.setCursor(0,20); display.print("Temperature:"); display.setCursor(72,20); display.print(temperature); display.setCursor(0,30); display.print("Humidity:"); display.setCursor(54,30); display.print(humidity); display.setCursor(0,40); display.print("Pressure:"); display.setCursor(54,40); display.print(pressure); display.setCursor(0,50); display.print("Reading ID:"); display.setCursor(66,50); display.print(readingID); display.display(); Serial.print("Sending packet: "); Serial.println(readingID); readingID++; } void setup() { //initialize Serial Monitor Serial.begin(115200); startOLED(); startBME(); startLoRA(); } void loop() { getReadings(); sendReadings(); delay(10000); }
Start by including the necessary libraries for LoRa, OLED display and BME280 sensor.
//Libraries for LoRa #include <SPI.h> #include <LoRa.h> //Libraries for OLED Display #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> //Libraries for BME280 #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h>
Define the pins used by the LoRa transceiver module. We’re using the TTGO LoRa32 SX1276 OLED board V1.0 and these are the pins used by the LoRa chip:
//define the pins used by the LoRa transceiver module #define SCK 5 #define MISO 19 #define MOSI 27 #define SS 18 #define RST 14 #define DIO0 26
Note: if you’re using another LoRa board, check the pins used by the LoRa transceiver chip.
Select the LoRa frequency:
#define BAND 866E6Define the OLED pins.
#define OLED_SDA 4 #define OLED_SCL 15 #define OLED_RST 16
Define the OLED size.
#define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels
Define the pins used by the BME280 sensor.
//BME280 definition #define SDA 21 #define SCL 13
Create an I2C instance for the BME280 sensor and a bme object.
TwoWire I2Cone = TwoWire(1); Adafruit_BME280 bme;
Create some variables to hold the LoRa message, temperature, humidity, pressure and reading ID.
int readingID = 0; int counter = 0; String LoRaMessage = ""; float temperature = 0; float humidity = 0; float pressure = 0;
Create a display object for the OLED display.
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);
In the setup(), we call several functions that were created previously in the code to initialize the OLED display, the BME280 and the LoRa transceiver module.
void setup() { Serial.begin(115200); startOLED(); startBME(); startLoRA(); }
In the loop(), we call the getReadings() and sendReadings() functions that were also previously created. These functions are responsible for getting readings from the BME280 sensor, and to send those readings via LoRa, respectively.
void loop() { getReadings(); sendReadings(); delay(10000); }
getReadings()
Getting sensor readings is as simple as using the readTemperature(), readHumidity(), and readPressure() methods on the bme object:
void getReadings(){ temperature = bme.readTemperature(); humidity = bme.readHumidity(); pressure = bme.readPressure() / 100.0F; }
sendReadings()
To send the readings via LoRa, we concatenate all the readings on a single variable, LoRaMessage:
void sendReadings() { LoRaMessage = String(readingID) + "/" + String(temperature) + "&" + String(humidity) + "#" + String(pressure);
Note that each reading is separated with a special character, so the receiver can easily identify each value.
Then, send the packet using the following:
LoRa.beginPacket(); LoRa.print(LoRaMessage); LoRa.endPacket();
Each time we send a LoRa packet, we increase the readingID variable so that we have an idea on how many packets were sent. You can delete this variable if you want.
readingID++;
The loop() is repeated every 10000 milliseconds (10 seconds). So, new sensor readings are sent every 10 seconds. You can change this delay time if you want.
delay(10000);
Upload the code to your ESP32 LoRa Sender Board.
Go to Tools > Port and select the COM port it is connected to. Then, go to Tools > Board and select the board you’re using. In our case, it’s the TTGO LoRa32-OLED V1.

Finally, press the upload button.
Open the Serial Monitor at a baud rate of 115200. You should get something as shown below.

The OLED of your board should be displaying the latest sensor readings.

Your LoRa Sender is ready. Now, let’s move on to the LoRa Receiver.
The LoRa Receiver gets incoming LoRa packets and displays the received readings on an asynchronous web server. Besides the sensor readings, we also display the last time those readings were received and the RSSI (received signal strength indicator).
The following figure shows the web server we’ll build.

As you can see, it contains a background image and styles to make the web page more appealing. There are several ways to display images on an ESP32 web server. We’ll store the image on the ESP32 filesystem (LittleFS). We’ll also store the HTML file on LittleFS.
To build the web server you need three different files: the Arduino sketch, the HTML file and the image. The HTML file and the image should be saved inside a folder called data inside the Arduino sketch folder, as shown below.

Create an index.html file with the following content or download all the project files here:
<!DOCTYPE HTML><html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="data:,"> <title>ESP32 (LoRa + Server)</title> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> <style> body { margin: 0; font-family: Arial, Helvetica, sans-serif; text-align: center; } header { margin: 0; padding-top: 5vh; padding-bottom: 5vh; overflow: hidden; background-image: url(winter); background-size: cover; color: white; } h2 { font-size: 2.0rem; } p { font-size: 1.2rem; } .units { font-size: 1.2rem; } .readings { font-size: 2.0rem; } </style> </head> <body> <header> <h2>ESP32 (LoRa + Server)</h2> <p><strong>Last received packet:<br/><span id="timestamp">%TIMESTAMP%</span></strong></p> <p>LoRa RSSI: <span id="rssi">%RSSI%</span></p> </header> <main> <p> <i class="fas fa-thermometer-half" style="color:#059e8a;"></i> Temperature: <span id="temperature" class="readings">%TEMPERATURE%</span> <sup>°C</sup> </p> <p> <i class="fas fa-tint" style="color:#00add6;"></i> Humidity: <span id="humidity" class="readings">%HUMIDITY%</span> <sup>%</sup> </p> <p> <i class="fas fa-angle-double-down" style="color:#e8c14d;"></i> Pressure: <span id="pressure" class="readings">%PRESSURE%</span> <sup>hpa</sup> </p> </main> <script> setInterval(updateValues, 10000, "temperature"); setInterval(updateValues, 10000, "humidity"); setInterval(updateValues, 10000, "pressure"); setInterval(updateValues, 10000, "rssi"); setInterval(updateValues, 10000, "timestamp"); function updateValues(value) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById(value).innerHTML = this.responseText; } }; xhttp.open("GET", "/" + value, true); xhttp.send(); } </script> </body> </html>
We’ve also included the CSS styles on the HTML file as well as some JavaScript that is responsible for updating the sensor readings automatically.
Something important to notice are the placeholders. The placeholders go between % signs: %TIMESTAMP%, %TEMPERATURE%, %HUMIDITY%, %PRESSURE% and %RSSI%.
These placeholders will then be replaced with the actual values by the Arduino code.
The styles are added between the <style> and </style> tags.
<style> body { margin: 0; font-family: Arial, Helvetica, sans-serif; text-align: center; } header { margin: 0; padding-top: 10vh; padding-bottom: 5vh; overflow: hidden; width: 100%; background-image: url(winter.jpg); background-size: cover; color: white; } h2 { font-size: 2.0rem; } p { font-size: 1.2rem; } .units { font-size: 1.2rem; } .readings { font-size: 2.0rem; } </style>
If you want a different image for your background, you just need to modify the following line to include your image’s name. In our case, it is called winter.jpg.
background-image: url(winter.jpg);
The JavaScript goes between the <scritpt> and </script> tags.
<script> setInterval(updateValues("temperature"), 5000); setInterval(updateValues("humidity"), 5000); setInterval(updateValues("pressure"), 5000); setInterval(updateValues("rssi"), 5000); setInterval(updateValues("timeAndDate"), 5000); function updateValues(value) { console.log(value); var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById(value).innerHTML = this.responseText; } }; xhttp.open("GET", "/" + value, true); xhttp.send(); } </script>
We won’t explain in detail how the HTML and CSS works, but a good place to learn is the W3Schools website.
Copy the following code to your Arduino IDE or download all the project files here. Then, you need to type your network credentials (SSID and password) to make it work.
/********* Rui Santos & Sara Santos - Random Nerd Tutorials Complete project details at https://RandomNerdTutorials.com/esp32-lora-sensor-web-server/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ // Import Wi-Fi library #include <WiFi.h> #include "ESPAsyncWebServer.h" #include <LittleFS.h> //Libraries for LoRa #include <SPI.h> #include <LoRa.h> //Libraries for OLED Display #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> // Libraries to get time from NTP Server #include <NTPClient.h> #include <WiFiUdp.h> //define the pins used by the LoRa transceiver module #define SCK 5 #define MISO 19 #define MOSI 27 #define SS 18 #define RST 14 #define DIO0 26 //433E6 for Asia //866E6 for Europe //915E6 for North America #define BAND 866E6 //OLED pins #define OLED_SDA 4 #define OLED_SCL 15 #define OLED_RST 16 #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels // Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Define NTP Client to get time WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP); // Variables to save date and time String formattedDate; String day; String hour; String timestamp; // Initialize variables to get and save LoRa data int rssi; String loRaMessage; String temperature; String humidity; String pressure; String readingID; // Create AsyncWebServer object on port 80 AsyncWebServer server(80); Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST); // Replaces placeholder with DHT values String processor(const String& var){ //Serial.println(var); if(var == "TEMPERATURE"){ return temperature; } else if(var == "HUMIDITY"){ return humidity; } else if(var == "PRESSURE"){ return pressure; } else if(var == "TIMESTAMP"){ return timestamp; } else if (var == "RRSI"){ return String(rssi); } return String(); } //Initialize OLED display void startOLED(){ //reset OLED display via software pinMode(OLED_RST, OUTPUT); digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH); //initialize OLED Wire.begin(OLED_SDA, OLED_SCL); if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32 Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } display.clearDisplay(); display.setTextColor(WHITE); display.setTextSize(1); display.setCursor(0,0); display.print("LORA SENDER"); } //Initialize LoRa module void startLoRA(){ int counter; //SPI LoRa pins SPI.begin(SCK, MISO, MOSI, SS); //setup LoRa transceiver module LoRa.setPins(SS, RST, DIO0); while (!LoRa.begin(BAND) && counter < 10) { Serial.print("."); counter++; delay(500); } if (counter == 10) { // Increment readingID on every new reading Serial.println("Starting LoRa failed!"); } Serial.println("LoRa Initialization OK!"); display.setCursor(0,10); display.clearDisplay(); display.print("LoRa Initializing OK!"); display.display(); delay(2000); } void connectWiFi(){ // Connect to Wi-Fi network with SSID and password Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } // Print local IP address and start web server Serial.println(""); Serial.println("WiFi connected."); Serial.println("IP address: "); Serial.println(WiFi.localIP()); display.setCursor(0,20); display.print("Access web server at: "); display.setCursor(0,30); display.print(WiFi.localIP()); display.display(); } // Read LoRa packet and get the sensor readings void getLoRaData() { Serial.print("Lora packet received: "); // Read packet while (LoRa.available()) { String LoRaData = LoRa.readString(); // LoRaData format: readingID/temperature&soilMoisture#batterylevel // String example: 1/27.43&654#95.34 Serial.print(LoRaData); // Get readingID, temperature and soil moisture int pos1 = LoRaData.indexOf('/'); int pos2 = LoRaData.indexOf('&'); int pos3 = LoRaData.indexOf('#'); readingID = LoRaData.substring(0, pos1); temperature = LoRaData.substring(pos1 +1, pos2); humidity = LoRaData.substring(pos2+1, pos3); pressure = LoRaData.substring(pos3+1, LoRaData.length()); } // Get RSSI rssi = LoRa.packetRssi(); Serial.print(" with RSSI "); Serial.println(rssi); } // Function to get date and time from NTPClient void getTimeStamp() { while(!timeClient.update()) { timeClient.forceUpdate(); } // The formattedDate comes with the following format: // 2018-05-28T16:00:13Z // We need to extract date and time formattedDate = timeClient.getFormattedDate(); Serial.println(formattedDate); // Extract date int splitT = formattedDate.indexOf("T"); day = formattedDate.substring(0, splitT); Serial.println(day); // Extract time hour = formattedDate.substring(splitT+1, formattedDate.length()-1); Serial.println(hour); timestamp = day + " " + hour; } void setup() { // Initialize Serial Monitor Serial.begin(115200); startOLED(); startLoRA(); connectWiFi(); if(!LittleFS.begin()){ Serial.println("An Error has occurred while mounting LittleFS"); return; } // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/index.html", String(), false, processor); }); server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", temperature.c_str()); }); server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", humidity.c_str()); }); server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", pressure.c_str()); }); server.on("/timestamp", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", timestamp.c_str()); }); server.on("/rssi", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", String(rssi).c_str()); }); server.on("/winter", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/winter.jpg", "image/jpg"); }); // Start server server.begin(); // Initialize a NTPClient to get time timeClient.begin(); // Set offset time in seconds to adjust for your timezone, for example: // GMT +1 = 3600 // GMT +8 = 28800 // GMT -1 = -3600 // GMT 0 = 0 timeClient.setTimeOffset(0); } void loop() { // Check if there are LoRa packets available int packetSize = LoRa.parsePacket(); if (packetSize) { getLoRaData(); getTimeStamp(); } }
You start by including the necessary libraries. You need libraries to:
// Import Wi-Fi library #include <WiFi.h> #include "ESPAsyncWebServer.h" #include <LittleFS.h> //Libraries for LoRa #include <SPI.h> #include <LoRa.h> //Libraries for OLED Display #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> // Libraries to get time from NTP Server #include <NTPClient.h> #include <WiFiUdp.h>
Define the pins used by the LoRa transceiver module.
#define SCK 5 #define MISO 19 #define MOSI 27 #define SS 18 #define RST 14 #define DIO0 26
Note: if you’re using another LoRa board, check the pins used by the LoRa transceiver chip.
Define the LoRa frequency:
//433E6 for Asia //866E6 for Europe //915E6 for North America #define BAND 866E6
Set up the OLED pins:
#define OLED_SDA 4 #define OLED_SCL 15 #define OLED_RST 16 #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels
Enter your network credentials in the following variables so that the ESP32 can connect to your local network.
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Define an NTP Client to get date and time:
WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP);
Create variables to save date and time:
String formattedDate; String day; String hour; String timestamp;
More variables to store the sensor readings received via LoRa radio.
int rssi; String loRaMessage; String temperature; String humidity; String pressure; String readingID;
Create an AsyncWebServer object called server on port 80.
AsyncWebServer server(80);
Create an object called display for the OLED display:
AsyncWebServer server(80);
The processor() function is what will attribute values to the placeholders we’ve created on the HTML file.
It accepts as argument the placeholder and should return a String that will replace that placeholder.
For example, if it finds the TEMPERATURE placeholder, it will return the temperature String variable.
// Replaces placeholder with DHT values String processor(const String& var){ //Serial.println(var); if(var == "TEMPERATURE"){ return temperature; } else if(var == "HUMIDITY"){ return humidity; } else if(var == "PRESSURE"){ return pressure; } else if(var == "TIMESTAMP"){ return timestamp; } else if (var == "RRSI"){ return String(rssi); } return String(); }
In the setup(), you initialize the OLED display, the LoRa communication, and connect to Wi-Fi.
void setup() { // Initialize Serial Monitor Serial.begin(115200); startOLED(); startLoRA(); connectWiFi();
You also initialize LittleFS:
if(!LittleFS.begin()){ Serial.println("An Error has occurred while mounting LittleFS"); return; }
Async Web Server
The ESPAsyncWebServer library allows us to configure the routes where the server will be listening for incoming HTTP requests.
For example, when a request is received on the route URL, we send the index.html file that is saved in the ESP32 LittleFS:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/index.html", String(), false, processor); });
As mentioned previously, we added a bit of Javascript to the HTML file that is responsible for updating the web page every 10 seconds. When that happens, it makes a request on the /temperature, /humidity, /pressure, /timestamp, /rssi URLs.
So, we need to handle what happens when we receive those requests. We simply need to send the temperature, humidity, pressure, timestamp and rssi variables. The variables should be sent in char format, that’s why we use the .c_str() method.
server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", temperature.c_str()); }); server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", humidity.c_str()); }); server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", pressure.c_str()); }); server.on("/timestamp", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", timestamp.c_str()); }); server.on("/rssi", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", String(rssi).c_str()); });
Because we included an image in the web page, we’ll get a request “asking” for the image. So, we need to send the image that is saved on the ESP32 LittleFS.
server.on("/winter", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/winter.jpg", "image/jpg"); });
Finally, start the web server.
server.begin();
Still in the setup(), create an NTP client to get the time from the internet.
timeClient.begin();
The time is returned in GMT format, so if you need to adjust for your timezone, you can use the following:
// Set offset time in seconds to adjust for your timezone, for example: // GMT +1 = 3600 // GMT +8 = 28800 // GMT -1 = -3600 // GMT 0 = 0 timeClient.setTimeOffset(0);
In the loop(), we listen for incoming LoRa packets:
int packetSize = LoRa.parsePacket();
If a new LoRa packet is available, we call the getLoRaData() and getTimeStamp() functions.
if (packetSize) { getLoRaData(); getTimeStamp(); }
The getLoRaData() function receives the LoRa message and splits it to get the different readings.
The getTimeStamp() function gets the time and date from the internet at the moment we receive the packet.
After inserting your network credentials, save your sketch. Then, in your Arduino IDE go to Sketch > Show Sketch Folder, and create a folder called data.

Inside that folder, you should have the HTML file and the image file.
After making sure you have all the needed files in the right directories, you need to upload the files to the ESP32 LittleFS filesystem.
Press [Ctrl] + [Shift] + [P] on Windows or [⌘] + [Shift] + [P] on MacOS to open the command palette. Search for the Upload LittleFS to Pico/ESP8266/ESP32 command and click on it.
If you don’t have this option it’s because you didn’t install the filesystem uploader plugin. Check this tutorial.

Important: make sure the Serial Monitor is closed before uploading to the filesystem. Otherwise, the upload will fail.
After a few seconds, the files should be successfully uploaded to LittleFS.
Now, upload the sketch to your board.
Open the Serial Monitor at a baud rate of 115200.
You should get the ESP32 IP address, and you should start receiving LoRa packets from the sender.

You should also get the IP address displayed on the OLED.

Open a browser and type your ESP32 IP address. You should see the web server with the latest sensor readings.

With these boards we were able to get a stable LoRa communication up to 180 meters (590 ft) in open field. These means that we can have the sender and receiver 180 meters apart and we’re still able to get and check the readings on the web server.

Getting a stable communication at a distance of 180 meters with such low cost boards and without any further customization is really impressive.
However, in a previous project using an RFM95 SX1276 LoRa transceiver chip with an home made antenna, we got better results: more than 250 meters with many obstacles in between.

The communication range will really depend on your environment, the LoRa board you’re using and many other variables.
Copyright ©2025. All Rights Reserved Emblab THE RAVE INNOVATION