Learn how to plot sensor readings (temperature, humidity, and pressure) on a web server using the ESP32 or ESP8266 with Arduino IDE. The ESP will host a web page with three real-time charts that have new readings added every 30 seconds.

In this tutorial we’ll build an asynchronous web server using the ESPAsyncWebServer library.
The HTML to build the web page will be stored on the ESP32 or ESP8266 Filesystem (LittleFS).
We’ll display temperature, humidity and pressure readings from a BME280 sensor on a chart, but you can modify this project to display sensor readings from any other sensor. To learn more about the BME280, read our guides:
To build the charts, we’ll use the Highcharts library. We’ll create three charts: temperature, humidity and pressure over time. The charts display a maximum of 40 data points, and a new reading is added every 30 seconds, but you change these values in your code.
To see how the project works, you can watch the following video demonstration:
Make sure you check all the prerequisites in this section before continuing with the project in order to compile the code.
We’ll program the ESP32 and ESP8266 using Arduino IDE. So, you must have the ESP32 or ESP8266 add-on installed. Follow one of the next tutorials to install the ESP add-on:
To upload the HTML file to the ESP32 and ESP8266 flash memory, we’ll use a plugin for Arduino IDE: Filesystem uploader. Follow one of the next tutorials to install the filesystem uploader plugin. The uploader is compatible with both boards.
To build the web server you need to install the following libraries:
You can install those libraries in the Arduino IDE Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the libraries’ names.
To get readings from the BME280 sensor module you need to have the next libraries installed:
You can install these libraries through the Arduino Library Manager.

To follow this tutorial you need the following parts:
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!
The BME280 sensor module we’re using communicates via I2C communication protocol, so you need to connect it to the ESP32 or ESP8266 I2C pins.
| BME280 | ESP32 |
| SCK (SCL Pin) | GPIO 22 |
| SDI (SDA pin) | GPIO 21 |
So, assemble your circuit as shown in the next schematic diagram.

Recommended reading: ESP32 Pinout Reference Guide
| BME280 | ESP8266 |
| SCK (SCL Pin) | GPIO 5 |
| SDI (SDA pin) | GPIO 4 |
Assemble your circuit as in the next schematic diagram if you’re using an ESP8266 board.

Recommended reading: ESP8266 Pinout Reference Guide
To build the web server you need two different files. The Arduino sketch and the HTML file. The HTML file 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 project files here:
<!DOCTYPE HTML><html> <!-- Rui Santos - Complete project details at https://RandomNerdTutorials.com 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. --> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="https://code.highcharts.com/highcharts.js"></script> <style> body { min-width: 310px; max-width: 800px; height: 400px; margin: 0 auto; } h2 { font-family: Arial; font-size: 2.5rem; text-align: center; } </style> </head> <body> <h2>ESP Weather Station</h2> <div id="chart-temperature" class="container"></div> <div id="chart-humidity" class="container"></div> <div id="chart-pressure" class="container"></div> </body> <script> var chartT = new Highcharts.Chart({ chart:{ renderTo : 'chart-temperature' }, title: { text: 'BME280 Temperature' }, series: [{ showInLegend: false, data: [] }], plotOptions: { line: { animation: false, dataLabels: { enabled: true } }, series: { color: '#059e8a' } }, xAxis: { type: 'datetime', dateTimeLabelFormats: { second: '%H:%M:%S' } }, yAxis: { title: { text: 'Temperature (Celsius)' } //title: { text: 'Temperature (Fahrenheit)' } }, credits: { enabled: false } }); setInterval(function ( ) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var x = (new Date()).getTime(), y = parseFloat(this.responseText); //console.log(this.responseText); if(chartT.series[0].data.length > 40) { chartT.series[0].addPoint([x, y], true, true, true); } else { chartT.series[0].addPoint([x, y], true, false, true); } } }; xhttp.open("GET", "/temperature", true); xhttp.send(); }, 30000 ) ; var chartH = new Highcharts.Chart({ chart:{ renderTo:'chart-humidity' }, title: { text: 'BME280 Humidity' }, series: [{ showInLegend: false, data: [] }], plotOptions: { line: { animation: false, dataLabels: { enabled: true } } }, xAxis: { type: 'datetime', dateTimeLabelFormats: { second: '%H:%M:%S' } }, yAxis: { title: { text: 'Humidity (%)' } }, credits: { enabled: false } }); setInterval(function ( ) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var x = (new Date()).getTime(), y = parseFloat(this.responseText); //console.log(this.responseText); if(chartH.series[0].data.length > 40) { chartH.series[0].addPoint([x, y], true, true, true); } else { chartH.series[0].addPoint([x, y], true, false, true); } } }; xhttp.open("GET", "/humidity", true); xhttp.send(); }, 30000 ) ; var chartP = new Highcharts.Chart({ chart:{ renderTo:'chart-pressure' }, title: { text: 'BME280 Pressure' }, series: [{ showInLegend: false, data: [] }], plotOptions: { line: { animation: false, dataLabels: { enabled: true } }, series: { color: '#18009c' } }, xAxis: { type: 'datetime', dateTimeLabelFormats: { second: '%H:%M:%S' } }, yAxis: { title: { text: 'Pressure (hPa)' } }, credits: { enabled: false } }); setInterval(function ( ) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var x = (new Date()).getTime(), y = parseFloat(this.responseText); //console.log(this.responseText); if(chartP.series[0].data.length > 40) { chartP.series[0].addPoint([x, y], true, true, true); } else { chartP.series[0].addPoint([x, y], true, false, true); } } }; xhttp.open("GET", "/pressure", true); xhttp.send(); }, 30000 ) ; </script> </html>
Let’s take a quick look at the relevant parts to build a chart.
First, you need to include the highcharts library:
<script src="https://code.highcharts.com/highcharts.js"></script>
You need to create a <div> section for each graphic with a unique id. In this case: chart-temperature, chart-humidity and chart-pressure.
<div id="chart-temperature" class="container"></div> <div id="chart-humidity" class="container"></div> <div id="chart-pressure" class="container"></div>
To create the charts and add data to the charts, we use javascript code. It should go inside the <script> and </script> tags.
The following spinet creates the temperature chart. You define the chart id, you can set the title, the axis labels, etc…
var chartT = new Highcharts.Chart({ chart:{ renderTo : 'chart-temperature' }, title: { text: 'BME280 Temperature' }, series: [{ showInLegend: false, data: [] }], plotOptions: { line: { animation: false, dataLabels: { enabled: true } }, series: { color: '#059e8a' } }, xAxis: { type: 'datetime', dateTimeLabelFormats: { second: '%H:%M:%S' } }, yAxis: { title: { text: 'Temperature (Celsius)' } //title: { text: 'Temperature (Fahrenheit)' } }, credits: { enabled: false } });
Then, the setInterval() function adds points to the charts. Every 30 seconds it makes a request to the /temperature URL to get the temperature readings from your ESP32 or ESP8266.
setInterval(function ( ) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var x = (new Date()).getTime(), y = parseFloat(this.responseText); //console.log(this.responseText); if(chartT.series[0].data.length > 40) { chartT.series[0].addPoint([x, y], true, true, true); } else { chartT.series[0].addPoint([x, y], true, false, true); } } }; xhttp.open("GET", "/temperature", true); xhttp.send(); }, 30000 ) ;
The other graphics are created in a similar way. We make a request on the /humidity and /pressure URLs to get the humidity and pressure readings, respectively.
In the Arduino sketch, we should handle what happens when we receive those requests: we should send the corresponding sensor readings.
Copy the following code to the Arduino IDE or download all 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-esp8266-plot-chart-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. *********/ #ifdef ESP32 #include <WiFi.h> #include <ESPAsyncWebServer.h> #include <LittleFS.h> #else #include <Arduino.h> #include <ESP8266WiFi.h> #include <Hash.h> #include <ESPAsyncTCP.h> #include <ESPAsyncWebServer.h> #include <LittleFS.h> #include <FS.h> #endif #include <Wire.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> /*#include <SPI.h> #define BME_SCK 18 #define BME_MISO 19 #define BME_MOSI 23 #define BME_CS 5*/ Adafruit_BME280 bme; // I2C //Adafruit_BME280 bme(BME_CS); // hardware SPI //Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK); // software SPI // Replace with your network credentials const char* ssid = "REPLACE_IWTH_YOUR_SSID"; const char* password = "REPLACE_IWTH_YOUR_PASSWORD"; // Create AsyncWebServer object on port 80 AsyncWebServer server(80); String readBME280Temperature() { // Read temperature as Celsius (the default) float t = bme.readTemperature(); // Convert temperature to Fahrenheit //t = 1.8 * t + 32; if (isnan(t)) { Serial.println("Failed to read from BME280 sensor!"); return ""; } else { Serial.println(t); return String(t); } } String readBME280Humidity() { float h = bme.readHumidity(); if (isnan(h)) { Serial.println("Failed to read from BME280 sensor!"); return ""; } else { Serial.println(h); return String(h); } } String readBME280Pressure() { float p = bme.readPressure() / 100.0F; if (isnan(p)) { Serial.println("Failed to read from BME280 sensor!"); return ""; } else { Serial.println(p); return String(p); } } void setup(){ // Serial port for debugging purposes Serial.begin(115200); bool status; // default settings // (you can also pass in a Wire library object like &Wire2) status = bme.begin(0x76); if (!status) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } // Initialize LittleFS if(!LittleFS.begin()){ Serial.println("An Error has occurred while mounting LittleFS"); return; } // Connect to Wi-Fi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } // Print ESP32 Local IP Address Serial.println(WiFi.localIP()); // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/index.html"); }); server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", readBME280Temperature().c_str()); }); server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", readBME280Humidity().c_str()); }); server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", readBME280Pressure().c_str()); }); // Start server server.begin(); } void loop(){ }
Let’s take a quick look at the code and see how it works.
First, include the necessary libraries. You include different libraries depending on the board you’re using. If you’re using an ESP32, the code loads the following libraries:
#include <WiFi.h> #include <ESPAsyncWebServer.h> #include <LittleFS.h>
If you’re using an ESP8266, the code loads these libraries:
#include <Arduino.h> #include <ESP8266WiFi.h> #include <Hash.h> #include <ESPAsyncTCP.h> #include <ESPAsyncWebServer.h> #include <LittleFS.h> #include <Wire.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h>
Create an instance to communicate with the BME280 sensor using I2C:
Adafruit_BME280 bme; // I2C
Insert your network credentials in the following variables:
// Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Create AsyncWebServer object on port 80:
AsyncWebServer server(80);
Then, we create three functions readBME280Temperature(), readBME280Humidity() and readBME280Pressure(). These functions request the temperature, humidity and pressure from the BME280 sensor and return the readings as a String type.
String readBME280Temperature() { // Read temperature as Celsius (the default) float t = bme.readTemperature(); // Convert temperature to Fahrenheit //t = 1.8 * t + 32; if (isnan(t)) { Serial.println("Failed to read from BME280 sensor!"); return ""; } else { Serial.println(t); return String(t); } }
In the setup(), initialize the sensor:
status = bme.begin(0x76); if (!status) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); }
Initialize the filesystem (LittleFS):
if(!LittleFS.begin()){ Serial.println("An Error has occurred while mounting LittleFS"); return; }
Connect to Wi-Fi and print the IP address in the Serial Monitor:
WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } // Print ESP32 Local IP Address Serial.println(WiFi.localIP());
Then, we need to handle what happens when the ESP receives a request.
When it receives a request on the root URL, we send the HTML text that is saved in LittleFS under the index.html name:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/index.html"); });
When we receive a request on the /temperature, /humidity or /pressure URLs, call the functions that return the sensor readings.
For example, if we receive a request on the /temperature URL, we call the readBME280Temperature() function that returns the temperature.
server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", readBME280Temperature().c_str()); });
The same happens for the other readings.
Finally, start the server:
server.begin();
Because this is an asynchronous web server we don’t need to write anything in the loop().
void loop(){ }
Save the code as ESP_Chart_Web_Server or download all project files here. Go to Sketch > Show Sketch Folder, and create a folder called data. Inside that folder you should save the HTML file created previously.

Now you need to upload the HTML file to the ESP32 or ESP8266 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 is 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.
Then, upload the code to your board. Make sure you have the right board and COM port selected. Also, make sure you’ve inserted your networks credentials in the code.
When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the board “EN/RST” button, and it should print its IP address.

Open a browser on your local network and type the ESP32 or ESP8266 IP address. You should see three charts. A new data point is added every 30 seconds to a total of 40 points. New data keeps being displayed on the charts as long as you have your web browser tab open.
Here is an example of the humidity chart:

You can select each point to see the exact timestamp.

Copyright ©2025. All Rights Reserved Emblab THE RAVE INNOVATION