ESP32 Neopixel Status Indicator and Sensor PCB Shield with Wi-Fi Manager

In this project, we’ll create a status indicator PCB shield for the ESP32 featuring two rows of addressable RGB neopixel LEDs, a BME280 sensor, and a pushbutton. We’ll program the board to display a web server with the BME280 sensor readings and show the temperature and humidity range on the LEDs (like two progress bars). We’ll also set up a Wi-Fi Manager⁠—the LEDs indicate whether it is already connected to a Wi-Fi network or if it is set in access point mode.

ESP32 Neopixel Status Indicator and Sensor PCB Shield with Wi-Fi Manager

The Wi-Fi Manager allows you to connect the ESP32 boards to different Access Points (networks) without having to hard-code network credentials (SSID and password) and upload new code to the board. Your ESP will automatically join the last saved network or set up an Access Point that you can use to configure the network credentials.

By following this project, you’ll learn more about the following concepts:

  • Controlling two addressable RGB LED “strips” individually with the ESP32;
  • Build a Web Server with the ESP32 using Server-sent Events (SSE);
  • Handle HTML input fields to save data on your board (SSID and password);
  • Save variables permanently using files on the filesystem;
  • Build your own Wi-Fi Manager using the ESPAsyncWebServer library;
  • Switch between station mode and access point mode;
  • And much more…

To better understand how this project works, we recommend taking a look at the following tutorials:

Watch the Video Tutorial

Resources

You can find all the resources needed to build this project in the links below (or you can visit the GitHub project page):

Project Overview

Before going straight to the project, let’s take a look at the PCB Shield features (hardware and software).

PCB Shield Features

The shield is designed with some headers pins to stack the ESP32 board. For this reason, if you want to build and use our PCB, you need to get the same ESP32 development board. We’re using the ESP32 DEVKIT DOIT V1 board (the model with 36 GPIOs).

ESP32 Status Indicator and Sensor Shield PCB

If you want to follow this project and have a different ESP32 model, you can assemble the circuit on a breadboard, or you can modify the PCB layout and wiring to match the pinout of your ESP32 board. Throughout this project, we provide all the necessary files if you need to modify the PCB.

Additionally, you can follow this project by assembling the circuit on a breadboard if you don’t want to build a PCB shield.

The shield consists of:

  • BME280 temperature, humidity, and pressure sensor;
  • Pushbutton;
  • Two rows of 5 addressable RGB LEDs (WS2812B).
ESP32 Status Indicator Sensor Shield RGB LEDs Overview

If you replicate this project on a breadboard, instead of individual WS2812B addressable RGB LEDs, you can use addressable RGB LED strips

PCB Shield Pin Assignment

The following table shows the pin assignment for each component on the shield:

ComponentESP32 Pin Assignment
BME280GPIO 21 (SDA), GPIO 22 (SCL)
PushbuttonGPIO 18
Addressable RGB LEDs (row 1)GPIO 27
Addressable RGB LEDs (row 2)GPIO 32

PCB Software Features

You can program the shield in several different ways. We’ll program the ESP32 to have the following features:

Web Server

Web server to display BME280 sensor readings: temperature, humidity, and pressure. It also displays the time of the last update. The readings update automatically every 30 seconds using server-sent events. Learn more about Server-Sent Events in this project.

ESP32 BME280 Sensor Readings Web Server Table

Visual Interface (Addressable RGB LEDs)

The RGB LEDs on the shield behave like a progress bar showing the range of temperature and humidity values. The higher the temperature, the more LEDs will be lit—the same for the humidity readings. The temperature values are displayed in an orange/yellow color, and the humidity is displayed in a teal color.

ESP32 Status Indicator Sensor Shield RGB LEDs Overview Sensor Readings

Wi-Fi Manager

The Wi-Fi Manager allows you to connect the ESP32 board to different Access Points (networks) without having to hard-code network credentials (SSID and password) and upload new code to your board. Your ESP will automatically join the last saved network or set up an Access Point that you can use to configure the network credentials.

ESP32 Wi-Fi Manager Web Page web server SSID password

When the board is in Access Point mode (Wi-Fi Manager), all LEDs are lit in red. When the board is in station mode (Web Server with sensor readings), all LEDs are temporarily lit in green/teal color before showing the temperature and humidity range.

ESP32 Status Indicator Sensor Shield RGB LEDs Wi-Fi Manager

Testing the Circuit on a Breadboard

Before designing and building the PCB, it’s important to test the circuit on a breadboard. If you don’t want to make a PCB, you can still follow this project by assembling the circuit on a breadboard.

Parts Required

To assemble the circuit on a breadboard you need the following parts (the parts for the PCB are shown in a later section):

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!

After gathering all the parts, assemble the circuit by following the next schematic diagram:

ESP32 Status Indicator Sensor Shield RGB LEDs Shield Breadboard Diagram

*We ended up not using the pushbutton for this particular project, so, it is not necessary to include it in your circuit.


Designing the PCB

To design the circuit and PCB, we used EasyEDA, a browser-based software, to design PCBs. If you want to customize your PCB, you need to upload the following files:

I’m not an expert in PCB design. However, designing simple PCBs like the one we’re using in this tutorial is straightforward. Designing the circuit works like in any other circuit software tool, you place some components, and you wire them together. Then, you assign each component to a footprint.

EasyEDA Circuit Diagram ESP32 Neopixel Shield PCB

Having the parts assigned, place each component. When you’re happy with the layout, make all the connections and route your PCB.

Easyeda ESP32 Neopixels PCB Shield

Save your project and export the Gerber files.

Note: you can grab the project files and edit them to customize the shield for your own needs.

Ordering the PCBs at PCBWay

This project is sponsored by PCBWay. PCBWay is a full feature Printed Circuit Board manufacturing service.

Ordering the PCBs at PCBWay

Turn your DIY breadboard circuits into professional PCBs – get 10 boards for approximately $5 + shipping (which will vary depending on your country).

Once you have your Gerber files, you can order the PCB. Follow the next steps.

1. Download the Gerber files – click here to download the .zip file

2. Go to PCBWay website and open the PCB Instant Quote page. 

PCBWay Order PCB open instant quote page

3. PCBWay can grab all the PCB details and automatically fills them for you. Use the “Quick-order PCB (Autofill parameters)”.

PCBWay Order PCB autofill parameters

4. Press the “+ Add Gerber file” button to upload the provided Gerber files.

PCBWay Order PCB add gerber file button

And that’s it. You can also use the OnlineGerberViewer to check if your PCB is looking as it should.

ESP32 Neopixel PCB Shield PCBWay Gerber Viewer

If you aren’t in a hurry, you can use the China Post shipping method to lower your cost significantly. In our opinion, we think they overestimate the China Post shipping time.

PCBWay Order PCB China post shipping method

You can increase your PCB order quantity and change the solder mask color. As usual, we’ve ordered the Blue color.

ESP32 Neopixel PCB Shield PCBWay Details

Once you’re ready, you can order the PCBs by clicking “Save to Cart” and complete your order.

Unboxing the PCBs

After approximately one week using the DHL shipping method, I received the PCBs at my office.

Status Indicator and Sensor Shield Unboxing PCBWay

As usual, everything comes well packed, and the PCBs are really high-quality. The letters on the silkscreen are really well-printed and easy to read.

We’re really satisfied with the PCBWay service. Here are some other projects we’ve built using the PCBWay service:

Soldering the Components

In our PCB, we’ve used SMD LEDs, SMD resistors, and SMD capacitors. These can be a bit difficult to solder, but the PCB looks much better. If you’ve never soldered SMD before, we recommend watching a few videos to learn how it’s done. You can also get an SMD DIY soldering kit to practice a bit.

Here’s a list of all the components needed to assemble the PCB:

ESP32 Status Indicator and Sensor Shield parts

Here are the soldering tools we’ve used:

TS80 mini portable soldering iron

Start by soldering the SMD components. Then, solder the header pins. And finally, solder the other components or use header pins if you don’t want to connect the components permanently.

Here’s how the ESP32 Shield looks like after assembling all the parts.

ESP32 Board PCB Shield Neopixels

The ESP32 board should stack perfectly on the header pins on the other side of the PCB.

Status Indicator PCB Shield Stack ESP32

Programming the Shield

As mentioned previously, we’ll program the board to have the following features:

  • Web Server to display BME280 sensor readings (station mode);
  • Visual Interface (Addressable RGB LEDs): the RGB LEDs on the shield behave like two progress bars showing the range of temperature and humidity values;
  • Wi-Fi Manager: the ESP32 will automatically join the last saved network or set up an Access Point that you can use to configure the network credentials.

The following diagram summarizes how the project works.

ESP32 Shield WiFi Manager How it Works
  • When the ESP first starts, it tries to read the ssid.txt, pass.txt, and ip.txt files (1);
  • If the files are empty (2) (the first time you run the board, the files are empty), your board is set as an access point, and all LEDs are light up in red color (3);
  • Using any Wi-Fi enabled device with a browser, you can connect to the newly created Access Point (default name ESP-WIFI-MANAGER);
  • After establishing a connection with the ESP-WIFI-MANAGER, you can go to the default IP address 192.168.4.1 to open a web page that allows you to configure your SSID and password (4);
  • The SSID, password, and IP address submitted on the form are saved on the corresponding files: ssid.txt, pass.txt, and ip.txt (5);
  • After that, the ESP board restarts (6);
  • This time, after restarting, the files are not empty, so the ESP will try to connect to the Wi-Fi network in station mode using the settings you’ve inserted on the form (7);
  • If it establishes a connection, the process is completed successfully (8) (all the LEDs are temporarily lit in green/teal color);
  • You can access the main web page that shows sensor readings (9), and the LEDs are light up accordingly to the temperature and humidity range (10). Otherwise, it will set the Access Point (3), and you can access the default IP address (192.168.4.1) to add another SSID/password combination.

Prerequisites

We’ll program the ESP32 board using Arduino IDE. So make sure you have the ESP32 board add-on installed.

If you want to program the ESP32/ESP8266 using VS Code + PlatformIO, follow the next tutorial:

Installing Libraries (Arduino IDE)

For this project, you need to install all these libraries in your Arduino IDE.

You can install the first four libraries using the Arduino Library Manager. Go to Sketch > Include Library > Manage Libraries and search for the library name.

The ESPAsyncWebServer and AsynTCP libraries aren’t available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch > Include Library > Add .zip Library and select the libraries you’ve just downloaded.

Installing Libraries (VS Code + PlatformIO)

If you’re programming the ESP32 using PlatformIO, you should include the libraries on the platformio.ini file like this:

[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps = adafruit/Adafruit NeoPixel @ ^1.7.0
           adafruit/Adafruit Unified Sensor @ ^1.1.4
           adafruit/Adafruit BME280 Library @ ^2.1.2
           arduino-libraries/Arduino_JSON @ 0.1.0

Filesystem Uploader

Before proceeding, you need to have the ESP32 Uploader Plugin installed in your Arduino IDE. Follow the next tutorial before proceeding:

If you’re using VS Code with PlatformIO, follow the next tutorial to learn how to upload files to the filesystem:

Organizing your Files

To keep the project organized and make it easier to understand, we’ll create five different files to build the web server:

  • Arduino sketch that handles the web server;
  • index.html: to define the content of the web page in station mode to display sensor readings;
  • style.css: to style the web page;
  • script.js: to program the behavior of the web page—handle web server responses, events, update the time, etc.;
  • wifimanager.html: to define the web page’s content to display the Wi-Fi Manager when the ESP32 is in access point mode.
ESP32 Web Server Wi-Fi Manager Files

You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We’ll upload these files to the ESP32 filesystem (SPIFFS).

You can download all project files:

Creating the HTML Files

For this project, you need two HTML files. One to build the main page that displays the sensor readings (index.html) and another to build the Wi-Fi Manager page (wifimanager.html).

index.html

Copy the following to the index.html file.

<!DOCTYPE html>
<html>
  <head>
    <title>ESP IOT DASHBOARD</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/png" href="favicon.png">
      <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <link rel="stylesheet" type="text/css" href="style.css">
  </head>
  <body>
    <div class="topnav">
      <h1>ESP WEB SERVER SENSOR READINGS</h1>
    </div>
    <div class="content">
      <div class="card-grid">
        <div class="card">
          <p class="card-title">BME280 Sensor Readings</p>
          <p>
            <table>
              <tr>
                <th>READING</th>
                <th>VALUE</th>
              </tr>
              <tr>
                <td>Temperature</td>
                <td><span id="temp"></span> &deg;C</td>
              </tr>
              <tr>
                <td>Humidity</td>
                <td><span id="hum"></span> &percnt;</td>
              </tr>
              <tr>
                <td>Pressure</td>
                <td><span id="pres"></span> hPa</td>
              </tr>
            </table>
          </p>
          <p class="update-time">Last update: <span id="update-time"></span></p>  
        </div>
      </div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

View raw code

This file creates a table to display the sensor readings. Here’s the paragraph that displays the table:

<p>
  <table>
    <tr>
      <th>READING</th>
      <th>VALUE</th>
    </tr>
    <tr>
      <td>Temperature</td>
      <td><span id="temp"></span> &deg;C</td>
    </tr>
    <tr>
       <td>Humidity</td>
       <td><span id="hum"></span> &percnt;</td>
    </tr>
    <tr>
      <td>Pressure</td>
      <td><span id="pres"></span> hPa</td>
    </tr>
  </table>
</p>

To create a table in HTML, start with the <table> and </table> tags. This encloses the entire table. To create a row, use the <tr> and </tr> tags. The table is defined with a series of rows. Use the <tr></tr> pair to enclose each row of data. The table heading is defined using the <th> and </th> tags, and each table cell is defined using the <td> and </td> tags.

Notice that the cells to display the sensor readings have <span> tags with specific ids to manipulate them later using JavaScript to insert the updated readings. For example, the cell for the temperature value has the id temp.

<td><span id="temp"></span> &deg;C</td>

Finally, there’s a paragraph to display the last time the readings were updated:

<p class="update-time">Last update: <span id="update-time"></span></p>

There’s a <span> tag with the update-time id. This will be used later to insert the date and time using JavaScript.

wifimanager.html

Copy the following to the wifimanager.html file.

<!DOCTYPE html>
<html>
<head>
  <title>ESP Wi-Fi Manager</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
  <div class="topnav">
    <h1>ESP Wi-Fi Manager</h1>
  </div>
  <div class="content">
    <div class="card-grid">
      <div class="card">
        <form action="/" method="POST">
          <p>
            <label for="ssid">SSID</label>
            <input type="text" id ="ssid" name="ssid"><br>
            <label for="pass">Password</label>
            <input type="text" id ="pass" name="pass"><br>
            <label for="ip">IP Address</label>
            <input type="text" id ="ip" name="ip" value="192.168.1.200">
            <input type ="submit" value ="Submit">
          </p>
        </form>
      </div>
    </div>
  </div>
  <script src="script.js"></script>
</body>
</html>

View raw code

This file creates an HTML form to insert the SSID and password of the network you want the ESP32 to join. You can also insert the IP address that you want your ESP32 to have. The following image shows the Wi-Fi Manager web page— three input fields and a submit button.

ESP32 Wi-Fi Manager Web Page

We want to send the submitted values on the input fields to the server when we click on the Submit button.

Here’s the HTML form with the three input fields:

<form action="/" method="POST">
  <p>
    <label for="ssid">SSID</label>
    <input type="text" id ="ssid" name="ssid"><br>
    <label for="pass">Password</label>
    <input type="text" id ="pass" name="pass"><br>
    <label for="ip">IP Address</label>
    <input type="text" id ="ip" name="ip" value="192.168.1.200">
    <input type ="submit" value ="Submit">
  </p>
</form>

This is the input field for the SSID:

<label for="ssid">SSID</label>
<input type="text" id ="ssid" name="ssid"><br>

The input field for the password:

<label for="pass">Password</label>
<input type="text" id ="pass" name="pass"><br>

The HTML form contains different form elements. All the form elements are enclosed inside this <form> tag. It contains controls (the input fields) and labels for those controls.

Additionally, the <form> tag must include the action attribute that specifies what you want to do when the form is submitted (it redirects to the / root URL, so that we remain on the same page). In our case, we want to send that data to the server (ESP32) when the user clicks the Submit button. The method attribute specifies the HTTP method (GET or POST) used when submitting the form data. In this case, we’ll use HTTP POST method.

<form action="/" method="POST">

POST is used to send data to a server to create/update a resource. The data sent to the server with POST is stored in the request body of the HTTP request. In this case, after submitting the values in the input fields, the body of the HTTP POST request would look like this:

POST /
Host: localhost
ssid: YOUR-NETWORK-SSID
pass: YOUR-PASSWORD
ip: IP-ADDRESS

The <input type=”submit” value=”Submit”> creates a submit button with the text “Submit”. When you click this button, the data submitted in the form is sent to the server.

<input type ="submit" value ="Submit">

And finally, there is an input field for the IP address you want to attribute to the ESP in station mode. By default, we set it to 192.168.1.200. You can set another default IP address or delete the value parameter—it won’t have a default value, the network automatically assigns a valid IP address to your board.

<label for="ip">IP Address</label>
<input type="text" id ="ip" name="ip" value="192.168.1.200">

Creating the CSS File

Copy the following styles to your style.css file.

html {
  font-family: Arial, Helvetica, sans-serif; 
  display: inline-block; 
  text-align: center;
}
h1 {
  font-size: 1.8rem; 
  color: white;
}
p { 
  font-size: 1.4rem;
}
.topnav { 
  overflow: hidden; 
  background-color: #0A1128;
}
body {  
  margin: 0;
}
.content { 
  padding: 50px;
}
.card-grid { 
  max-width: 800px; 
  margin: 0 auto; 
  display: grid; 
  grid-gap: 2rem; 
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card { 
  background-color: white; 
  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title { 
  font-size: 1.2rem;
  font-weight: bold;
  color: #034078
}
th, td {
  text-align: center;
  padding: 8px;
}
tr:nth-child(even) {
  background-color: #f2f2f2
}
tr:hover {
  background-color: #ddd;
}  
th {
  background-color: #50b8b4;
  color: white;
}
table {
  margin: 0 auto;
  width: 90%
}
.update-time {
  font-size: 0.8rem;
  color:#1282A2;
}

View raw code

This file styles the previous web pages.

Creating the JavaScript File

Copy the following to the script.js file.

// Get current sensor readings when the page loads  
window.addEventListener('load', getReadings);

//Function to add date and time of last update
function updateDateTime() {
  var currentdate = new Date(); 
  var datetime =  currentdate.getDate() + "/"
  + (currentdate.getMonth()+1)  + "/" 
  + currentdate.getFullYear() + " at "  
  + currentdate.getHours() + ":"  
  + currentdate.getMinutes() + ":" 
  + currentdate.getSeconds();
  document.getElementById("update-time").innerHTML = datetime;
  console.log(datetime);
}

// Function to get current readings on the webpage when it loads for the first time
function getReadings() {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var myObj = JSON.parse(this.responseText);
      console.log(myObj);
      document.getElementById("temp").innerHTML = myObj.temperature;
      document.getElementById("hum").innerHTML = myObj.humidity;
      document.getElementById("pres").innerHTML = myObj.pressure;
      updateDateTime();
    }
  };
  xhr.open("GET", "/readings", true);
  xhr.send();
}

// Create an Event Source to listen for events
if (!!window.EventSource) {
  var source = new EventSource('/events');

  source.addEventListener('open', function(e) {
    console.log("Events Connected");
  }, false);

  source.addEventListener('error', function(e) {
    if (e.target.readyState != EventSource.OPEN) {
      console.log("Events Disconnected");
    }
  }, false);

  source.addEventListener('new_readings', function(e) {
    console.log("new_readings", e.data);
    var obj = JSON.parse(e.data);
    document.getElementById("temp").innerHTML = obj.temperature;
    document.getElementById("hum").innerHTML = obj.humidity;
    document.getElementById("pres").innerHTML = obj.pressure;
    updateDateTime();
  }, false);
}

View raw code

This JavaScript handles the events sent by the server and updates the sensor readings on the corresponding places. It also requests date and time whenever a new reading is available. This file also makes a request to the latest sensor readings when you open a connection with the server.

Let’s take a click look at the JavaScript file and see how it works.

Get Readings

When you access the web page for the first time, it makes a request to the server to get the current sensor readings. Otherwise, we would have to wait for new sensor readings to arrive (via Server-Sent Events), which can take some time depending on the interval that you set on the server.

Add an event listener that calls the getReadings function when the web page loads.

window.addEventListener('load', getReadings);

The window object represents an open window in a browser. The addEventListener() method sets up a function to be called when a certain event happens. In this case, we’ll call the getReadings function when the pages loads (‘load’) to get the current sensor readings.

updateDateTime() function

The updateDateTime() function gets the current date and time and places it in the HTML element with the update-time id.

function updateDateTime() {
  var currentdate = new Date(); 
  var datetime =  currentdate.getDate() + "/"
  + (currentdate.getMonth()+1)  + "/" 
  + currentdate.getFullYear() + " at "  
  + currentdate.getHours() + ":"  
  + currentdate.getMinutes() + ":" 
  + currentdate.getSeconds();
  document.getElementById("update-time").innerHTML = datetime;
  console.log(datetime);
}

getReadings() function

Now, let’s take a look at the getReadings function. It sends a GET request to the server on the /readings URL and handles the response—a JSON string containing the sensor readings. It also places the temperature, humidity and pressure values on the HTML elements with the corresponding ids.

function getReadings() {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var myObj = JSON.parse(this.responseText);
      console.log(myObj);
      document.getElementById("temp").innerHTML = myObj.temperature;
      document.getElementById("hum").innerHTML = myObj.humidity;
      document.getElementById("pres").innerHTML = myObj.pressure;
      updateDateTime();
    }
  };
  xhr.open("GET", "/readings", true);
  xhr.send();
}

Handle Events

Now, we need to handle the events sent by the server (Server-Sent Events).

Create a new EventSource object and specify the URL of the page sending the updates. In our case, it’s /events.

if (!!window.EventSource) {
  var source = new EventSource('/events');

Once you’ve instantiated an event source, you can start listening for messages from the server with addEventListener().

These are the default event listeners, as shown in the AsyncWebServer documentation.

source.addEventListener('open', function(e) {
  console.log("Events Connected");
}, false);

source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
}, false);

Then, add an event listener for the ‘new_readings’ event.

source.addEventListener('new_readings', function(e) {
  console.log("new_readings", e.data);
  var obj = JSON.parse(e.data);
  document.getElementById("temp").innerHTML = obj.temperature;
  document.getElementById("hum").innerHTML = obj.humidity;
  document.getElementById("pres").innerHTML = obj.pressure;
  updateDateTime();
}, false);

When new readings are available, the ESP sends an event (‘new_readings’) to the client with a JSON string that contains the sensor readings.

The following line prints the content of the message on the console:

console.log("new_readings", e.data);

Then, convert the data into a JSON object with the parse() method and save it in the obj variable.

var obj = JSON.parse(e.data);

The JSON string comes in the following format:

{
  "temperature" : "25",
  "humidity" : "50",
  "pressure" : "1015"
}

You can get the temperature with obj.temperature, the humidity with obj.humidity and the pressure with obj.pressure.

The following lines put the received data into the elements with the corresponding ids (temp,hum and pres) on the web page.

document.getElementById("temp").innerHTML = obj.temperature;
document.getElementById("hum").innerHTML = obj.humidity;
document.getElementById("pres").innerHTML = obj.pressure;

Arduino Sketch

Copy the following code to your Arduino IDE or to the main.cpp file if your using PlatformIO.

/*********
  Rui Santos
  Complete instructions at https://RandomNerdTutorials.com/esp32-status-indicator-sensor-pcb/
  
  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.
*********/

#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

// Create an Event Source on /events
AsyncEventSource events("/events");

// Search for parameter in HTTP POST request
const char* PARAM_INPUT_1 = "ssid";
const char* PARAM_INPUT_2 = "pass";
const char* PARAM_INPUT_3 = "ip";

//Variables to save values from HTML form
String ssid;
String pass;
String ip;

// File paths to save input values permanently
const char* ssidPath = "/ssid.txt";
const char* passPath = "/pass.txt";
const char* ipPath = "/ip.txt";

IPAddress localIP;
//IPAddress localIP(192, 168, 1, 200); // hardcoded

// Set your Gateway IP address
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);

// Timer variables (check wifi)
unsigned long previousMillis = 0;
const long interval = 10000;  // interval to wait for Wi-Fi connection (milliseconds)

// WS2812B Addressable RGB LEDs
#define STRIP_1_PIN    27  // GPIO the LEDs are connected to
#define STRIP_2_PIN    32  // GPIO the LEDs are connected to
#define LED_COUNT  5  // Number of LEDs
#define BRIGHTNESS 50  // NeoPixel brightness, 0 (min) to 255 (max)
Adafruit_NeoPixel strip1(LED_COUNT, STRIP_1_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2(LED_COUNT, STRIP_2_PIN, NEO_GRB + NEO_KHZ800);

// Create a sensor object
Adafruit_BME280 bme;         // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)

//Variables to hold sensor readings
float temp;
float hum;
float pres;

// Json Variable to Hold Sensor Readings
JSONVar readings;

// Timer variables (get sensor readings)
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

//-----------------FUNCTIONS TO HANDLE SENSOR READINGS-----------------//

// Init BME280
void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

// Get Sensor Readings
void getSensorReadings(){
  temp = bme.readTemperature();
  hum = bme.readHumidity();
  pres= bme.readPressure()/100.0F;
}

// Return JSON String from sensor Readings
String getJSONReadings(){
  readings["temperature"] = String(temp);
  readings["humidity"] =  String(hum);
  readings["pressure"] = String(pres);
  String jsonString = JSON.stringify(readings);
  return jsonString;
}

//Update RGB LED colors accordingly to temp and hum values
void updateColors(){
  strip1.clear();
  strip2.clear();

  //Number of lit LEDs (temperature)
  int tempLEDs;
  if (temp<=0){
    tempLEDs = 1;
  }
  else if (temp>0 && temp<=10){
    tempLEDs = 2;
  }
  else if (temp>10 && temp<=20){
    tempLEDs = 3;
  }
  else if (temp>20 && temp<=30){
    tempLEDs = 4;
  }
  else{
    tempLEDs = 5;
  }

  //Turn on LEDs for temperature
  for(int i=0; i<tempLEDs; i++) {
    strip1.setPixelColor(i, strip1.Color(255, 165, 0));
    strip1.show();   
  }
  
  //Number of lit LEDs (humidity)
  int humLEDs = map(hum, 0, 100, 1, LED_COUNT);

  //Turn on LEDs for humidity
  for(int i=0; i<humLEDs; i++) { // For each pixel...
    strip2.setPixelColor(i, strip2.Color(25, 140, 200));
    strip2.show();
  }
}

//-----------------FUNCTIONS TO HANDLE SPIFFS AND FILES-----------------//

// Initialize SPIFFS
void initSPIFFS() {
  if (!SPIFFS.begin(true)) {
    Serial.println("An error has occurred while mounting SPIFFS");
  }
  else{
    Serial.println("SPIFFS mounted successfully");
  }
}

// Read File from SPIFFS
String readFile(fs::FS &fs, const char * path){
  Serial.printf("Reading file: %s\r\n", path);

  File file = fs.open(path);
  if(!file || file.isDirectory()){
    Serial.println("- failed to open file for reading");
    return String();
  }
  
  String fileContent;
  while(file.available()){
    fileContent = file.readStringUntil('\n');
    break;     
  }
  return fileContent;
}

// Write file to SPIFFS
void writeFile(fs::FS &fs, const char * path, const char * message){
  Serial.printf("Writing file: %s\r\n", path);

  File file = fs.open(path, FILE_WRITE);
  if(!file){
    Serial.println("- failed to open file for writing");
    return;
  }
  if(file.print(message)){
    Serial.println("- file written");
  } else {
    Serial.println("- frite failed");
  }
}

// Initialize WiFi
bool initWiFi() {
  if(ssid=="" || ip==""){
    Serial.println("Undefined SSID or IP address.");
    return false;
  }

  WiFi.mode(WIFI_STA);
  localIP.fromString(ip.c_str());

  if (!WiFi.config(localIP, gateway, subnet)){
    Serial.println("STA Failed to configure");
    return false;
  }
  WiFi.begin(ssid.c_str(), pass.c_str());
  Serial.println("Connecting to WiFi...");

  unsigned long currentMillis = millis();
  previousMillis = currentMillis;

  while(WiFi.status() != WL_CONNECTED) {
    currentMillis = millis();
    if (currentMillis - previousMillis >= interval) {
      Serial.println("Failed to connect.");
      return false;
    }
  }

  Serial.println(WiFi.localIP());
  return true;
}

void setup() {
  // Serial port for debugging purposes
  Serial.begin(115200);
  
  // Initialize strips
  strip1.begin();
  strip2.begin();
  
  // Set brightness 
  strip1.setBrightness(BRIGHTNESS);
  strip2.setBrightness(BRIGHTNESS);
  
  // Init BME280 senspr
  initBME();
  
  // Init SPIFFS
  initSPIFFS();

  // Load values saved in SPIFFS
  ssid = readFile(SPIFFS, ssidPath);
  pass = readFile(SPIFFS, passPath);
  ip = readFile(SPIFFS, ipPath);
  /*Serial.println(ssid);
  Serial.println(pass);
  Serial.println(ip);*/

  if(initWiFi()) {
    // If ESP32 inits successfully in station mode light up all pixels in a teal color
    for(int i=0; i<LED_COUNT; i++) { // For each pixel...
      strip1.setPixelColor(i, strip1.Color(0, 255, 128));
      strip2.setPixelColor(i, strip2.Color(0, 255, 128));

      strip1.show();   // Send the updated pixel colors to the hardware.
      strip2.show();   // Send the updated pixel colors to the hardware.
    }

    //Handle the Web Server in Station Mode
    // Route for root / web page
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
      request->send(SPIFFS, "/index.html", "text/html");
    });
    server.serveStatic("/", SPIFFS, "/");

    // Request for the latest sensor readings
    server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
      getSensorReadings();
      String json = getJSONReadings();
      request->send(200, "application/json", json);
      json = String();
    });

    events.onConnect([](AsyncEventSourceClient *client){
      if(client->lastId()){
        Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
      }
    });
    server.addHandler(&events);
    
    server.begin();
  }
  else {
    // else initialize the ESP32 in Access Point mode
    // light up all pixels in a red color
    for(int i=0; i<LED_COUNT; i++) { // For each pixel...
      strip1.setPixelColor(i, strip1.Color(255, 0, 0));
      strip2.setPixelColor(i, strip2.Color(255, 0, 0));
      //strip1.setPixelColor(i, strip1.Color(128, 0, 21));
      //strip2.setPixelColor(i, strip2.Color(128, 0, 21));

      strip1.show();   // Send the updated pixel colors to the hardware.
      strip2.show();   // Send the updated pixel colors to the hardware.
    }

    // Set Access Point
    Serial.println("Setting AP (Access Point)");
    // NULL sets an open Access Point
    WiFi.softAP("ESP-WIFI-MANAGER", NULL);

    IPAddress IP = WiFi.softAPIP();
    Serial.print("AP IP address: ");
    Serial.println(IP); 

    // Web Server Root URL For WiFi Manager Web Page
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
      request->send(SPIFFS, "/wifimanager.html", "text/html");
    });
    
    server.serveStatic("/", SPIFFS, "/");
    
    // Get the parameters submited on the form 
    server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) {
      int params = request->params();
      for(int i=0;i<params;i++){
        AsyncWebParameter* p = request->getParam(i);
        if(p->isPost()){
          // HTTP POST ssid value
          if (p->name() == PARAM_INPUT_1) {
            ssid = p->value().c_str();
            Serial.print("SSID set to: ");
            Serial.println(ssid);
            // Write file to save value
            writeFile(SPIFFS, ssidPath, ssid.c_str());
          }
          // HTTP POST pass value
          if (p->name() == PARAM_INPUT_2) {
            pass = p->value().c_str();
            Serial.print("Password set to: ");
            Serial.println(pass);
            // Write file to save value
            writeFile(SPIFFS, passPath, pass.c_str());
          }
          // HTTP POST ip value
          if (p->name() == PARAM_INPUT_3) {
            ip = p->value().c_str();
            Serial.print("IP Address set to: ");
            Serial.println(ip);
            // Write file to save value
            writeFile(SPIFFS, ipPath, ip.c_str());
          }
          //Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
        }
      }
      request->send(200, "text/plain", "Done. ESP will restart, connect to your router and go to IP address: " + ip);
      delay(3000);
      // After saving the parameters, restart the ESP32
      ESP.restart();
    });
    server.begin();
  }
}

void loop() {
  // If the ESP32 is set successfully in station mode...
  if (WiFi.status() == WL_CONNECTED) {

    //...Send Events to the client with sensor readins and update colors every 30 seconds
    if (millis() - lastTime > timerDelay) {
      getSensorReadings();
      updateColors();
      
      String message = getJSONReadings();
      events.send(message.c_str(),"new_readings" ,millis());
      lastTime = millis();
    }
  }
}

View raw code

How the Code Works

Let’s take a look at the code and see how it works.

First, include all the necessary libraries:

#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>
#include <Adafruit_BME280.h>

The following variables are used to search for the SSID, password, and IP address on the HTTP POST request made when the form is submitted.

// Search for parameter in HTTP POST request
const char* PARAM_INPUT_1 = "ssid";
const char* PARAM_INPUT_2 = "pass";
const char* PARAM_INPUT_3 = "ip";

The ssid, pass, and ip variables save the values of the SSID, password, and IP address submitted on the form.

// Variables to save values from HTML form
String ssid;
String pass;
String ip;

The SSID, password, and IP address, when submitted, are saved in files on the ESP filesystem. The following variables refer to the path of those files.

// File paths to save input values permanently
const char* ssidPath = "/ssid.txt";
const char* passPath = "/pass.txt";
const char* ipPath = "/ip.txt";

The station IP address is submitted on the Wi-Fi Manager form. However, you need to set the gateway and subnet in your code:

IPAddress localIP;
//IPAddress localIP(192, 168, 1, 200); // hardcoded

// Set your Gateway IP address
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);

WS2812B Settings

Define the pins that control the RGB LEDs. In this case, we have two individual rows connected to GPIOs 27 and 32.

#define STRIP_1_PIN    27  // GPIO the LEDs are connected to
#define STRIP_2_PIN    32  // GPIO the LEDs are connected to

Define the number of LEDs and the brightness. You can change the brightness if you want.

#define LED_COUNT  5  // Number of LEDs
#define BRIGHTNESS 20  // NeoPixel brightness, 0 (min) to 255 (max)

Finally, initialize two Adafruit_Neopixel objects to control each strip: strip1 (temperature) and strip2 (humidity):

Adafruit_NeoPixel strip1(LED_COUNT, STRIP_1_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2(LED_COUNT, STRIP_2_PIN, NEO_GRB + NEO_KHZ800);

initBME()

The initBME() function initializes the BME280 sensor on the ESP32 default I2C pins:

void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

Learn more about the BME280 sensor: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).

getSensorReadings()

The getSensorReadings() functions gets temperature, humidity, and pressure from the BME280 sensor and saves the values on the temp, hum, and pres variables.

// Get Sensor Readings
void getSensorReadings(){
  temp = bme.readTemperature();
  hum = bme.readHumidity();
  pres= bme.readPressure()/100.0F;
}

getJSONReadings()

The getJSONReadings() function returns a JSON string from the current temperature, humidity, and pressure values.

// Return JSON String from sensor Readings
String getJSONReadings(){
  readings["temperature"] = String(temp);
  readings["humidity"] =  String(hum);
  readings["pressure"] = String(pres);
  String jsonString = JSON.stringify(readings);
  return jsonString;
}

updateColors()

The updateColors() function lights up the RGB LEDs accordingly to the temperature and humidity values range.

First, you need to clear the strips using the clear() method:

strip1.clear();
strip2.clear();

We need to determine how many LEDs we want to light up, taking into account the temperature and humidity values. We save the number of LEDs to lighten up on the tempLEDs and humLEDs variables.

For the temperature, if the temperature is equal to or smaller than zero degrees Celsius, we light up one LED:

if (temp<=0){
  tempLEDs = 1;
}

Here are the other ranges:

  • 0<temperature=<10 –> 2 LEDs
  • 10<temperature=<20 –> 3 LEDs
  • 20<temperature=<30 –> 4 LEDs
  • 30<temperature–> 5 LEDs
//Number of lit LEDs (temperature)
int tempLEDs;
if (temp<=0){
  tempLEDs = 1;
}
else if (temp>0 && temp<=10){
  tempLEDs = 2;
}
else if (temp>10 && temp<=20){
  tempLEDs = 3;
}
else if (temp>20 && temp<=30){
      tempLEDs = 4;
}
else{
  tempLEDs = 5;
}

After determining how many LEDs should be lit, we need to actually light up those LEDs. To light up an LED, we can use the setPixelColor() method on the strip1 object followed by the show() method. We need a for loop to light all LEDs.

//Turn on LEDs for temperature
for(int i=0; i<tempLEDs; i++) {
  strip1.setPixelColor(i, strip1.Color(255, 165, 0));
  strip1.show();   
}

We follow a similar procedure for the humidity. First, determine how many LEDs should be lit:

//Number of lit LEDs (humidity)
int humLEDs = map(hum, 0, 100, 1, LED_COUNT);

And finally, light up the humidity LEDs (strip2):

for(int i=0; i<humLEDs; i++) { // For each pixel...
  strip2.setPixelColor(i, strip2.Color(25, 140, 200));
  strip2.show();
}

initSPIFFS()

This function initializes the ESP32 SPIFFS filesystem. In this project we save the HTML, CSS and JavaScript files to build the web server pages on the filesystem. We also have the .txt files to save the SSID, password and IP address.

void initSPIFFS() {
  if (!SPIFFS.begin(true)) {
    Serial.println("An error has occurred while mounting SPIFFS");
  }
  else{
    Serial.println("SPIFFS mounted successfully");
  }
}

readFile()

The readFile() function reads and returns the content of a file.

String readFile(fs::FS &fs, const char * path){
  Serial.printf("Reading file: %s\r\n", path);

  File file = fs.open(path);
  if(!file || file.isDirectory()){
    Serial.println("- failed to open file for reading");
    return String();
  }
  
  String fileContent;
  while(file.available()){
    fileContent = file.readStringUntil('\n');
    break;     
  }
  return fileContent;
}

writeFile()

The writeFile() functions writes content to a file.

void writeFile(fs::FS &fs, const char * path, const char * message){
  Serial.printf("Writing file: %s\r\n", path);

  File file = fs.open(path, FILE_WRITE);
  if(!file){
    Serial.println("- failed to open file for writing");
    return;
  }
  if(file.print(message)){
    Serial.println("- file written");
  } else {
    Serial.println("- frite failed");
  }
}

initWiFi()

The initWiFi() function returns a boolean value (either true or false) indicating if the ESP board connected successfully to a network.

First, it checks if the ssid and ip variables are empty. If they are, it won’t be able to connect to a network, so it returns false.

if(ssid=="" || ip==""){
  Serial.println("Undefined SSID or IP address.");
  return false;
}

If that’s not the case, we’ll try to connect to the network using the SSID and password saved on the ssid and pass variables and set the IP address.

WiFi.mode(WIFI_STA);
localIP.fromString(ip.c_str());

if (!WiFi.config(localIP, gateway, subnet)){
  Serial.println("STA Failed to configure");
  return false;
}
WiFi.begin(ssid.c_str(), pass.c_str());
Serial.println("Connecting to WiFi...");

If after 10 seconds (interval variable) , it is not able to connect to Wi-Fi, it will return false.

unsigned long currentMillis = millis();
previousMillis = currentMillis;

while(WiFi.status() != WL_CONNECTED) {
  currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    Serial.println("Failed to connect.");
    return false;
  }
}

Serial.println(WiFi.localIP());

If none of the previous conditions are met, it means that the ESP successfully connected to the network in station mode (returns true).

return true;

setup()

In the setup(), initialize the Serial Monitor.

Serial.begin(115200);

Initialize the rows of addressable RGB LEDs (strips):

// Initialize strips
strip1.begin();
strip2.begin();

Set the strips’ brightness. You can change the brightness on the BRIGTHNESS variable.

// Set brightness 
strip1.setBrightness(BRIGHTNESS);
strip2.setBrightness(BRIGHTNESS);

Call the initBME() function to initialize the sensor:

// Init BME280 senspr
initBME();

Initialize the filesystem:

// Init SPIFFS
initSPIFFS();

Read the files to get the previously saved SSID, password and IP address.

// Load values saved in SPIFFS
ssid = readFile(SPIFFS, ssidPath);
pass = readFile(SPIFFS, passPath);
ip = readFile(SPIFFS, ipPath);

If the ESP connects successfully in station mode (initWiFi() function returns true):

if(initWiFi()) {

Light up all LEDs in a teal color, so that we know that the ESP32 successfully connected to a Wi-Fi network:

// If ESP32 inits successfully in station mode light up all pixels in a teal color
for(int i=0; i<LED_COUNT; i++) { // For each pixel...
  strip1.setPixelColor(i, strip1.Color(0, 255, 128));
  strip2.setPixelColor(i, strip2.Color(0, 255, 128));

  strip1.show();   // Send the updated pixel colors to the hardware.
  strip2.show();   // Send the updated pixel colors to the hardware.
}

Then, we can set the commands to handle the web server requests. Send the index.html file, when you access the root URL:

//Handle the Web Server in Station Mode
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
  request->send(SPIFFS, "/index.html", "text/html");
});

Send the CSS and JavaScript files requested by the HTML file (that are also saved in SPIFFS):

server.serveStatic("/", SPIFFS, "/");

When you access the web server page for the first time, it makes a request to the server on the /readings URL asking for the latest sensor readings. When that happens, send the JSON string with the readings:

// Request for the latest sensor readings
server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
  getSensorReadings();
  String json = getJSONReadings();
  request->send(200, "application/json", json);
  json = String();
});

Set up the server-sent events on the server:

events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
});
server.addHandler(&events);

Finally, start the server:

server.begin();

If the ESP32 can’t connect to a Wi-Fi network, the initWiFi() function returns false. In this case, light up all the LEDs in a red color, so that we know the ESP32 will be in access point mode:

for(int i=0; i<LED_COUNT; i++) { // For each pixel...
  strip1.setPixelColor(i, strip1.Color(128, 0, 21));
  strip2.setPixelColor(i, strip2.Color(128, 0, 21));

  strip1.show();   // Send the updated pixel colors to the hardware.
  strip2.show();   // Send the updated pixel colors to the hardware.
}

Set up the ESP will as an access point:

// Set Access Point
Serial.println("Setting AP (Access Point)");
// NULL sets an open Access Point
WiFi.softAP("ESP-WIFI-MANAGER", NULL);

IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP); 

To set an access point, we use the softAP() method and pass as arguments the name for the access point and the password. We want the access point to be open, so we set the password to NULL. You can add a password if you want. To learn more about setting up an Access Point, read the following tutorial:

When you access the Access Point, it shows the web page to enter the network credentials on the form. So, the ESP must send the wifimanager.html file when it receives a request on the root / URL.

// Web Server Root URL For WiFi Manager Web Page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(SPIFFS, "/wifimanager.html", "text/html");
});

We must also handle what happens when the form is submitted via HTTP POST request.

The following lines save the submitted values on the ssid, pass, and ip variables and save those variables on the corresponding files.

// Get the parameters submited on the form 
server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) {
  int params = request->params();
  for(int i=0;i<params;i++){
    AsyncWebParameter* p = request->getParam(i);
    if(p->isPost()){
      // HTTP POST ssid value
      if (p->name() == PARAM_INPUT_1) {
        ssid = p->value().c_str();
        Serial.print("SSID set to: ");
        Serial.println(ssid);
        // Write file to save value
        writeFile(SPIFFS, ssidPath, ssid.c_str());
      }
      // HTTP POST pass value
      if (p->name() == PARAM_INPUT_2) {
        pass = p->value().c_str();
        Serial.print("Password set to: ");
        Serial.println(pass);
        // Write file to save value
        writeFile(SPIFFS, passPath, pass.c_str());
      }
      // HTTP POST ip value
      if (p->name() == PARAM_INPUT_3) {
        ip = p->value().c_str();
        Serial.print("IP Address set to: ");
        Serial.println(ip);
        // Write file to save value
        writeFile(SPIFFS, ipPath, ip.c_str());
      }
      //Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
    }
  }

After submitting the form, send a response with some text, so that we know that the ESP received the form details:

request->send(200, "text/plain", "Done. ESP will restart, connect to your router and go to IP address: " + ip);

After three seconds, restart the ESP board with ESP.restart():

delay(3000);
// After saving the parameters, restart the ESP32
ESP.restart();

After restarting, the board will have the SSID and password saved on the files, and it will successfully initialize in station mode.

loop()

In the loop(), check if the ESP32 is successfully connected to a wi-fi station:

if (WiFi.status() == WL_CONNECTED) {

If it is, do the following every 30 seconds (timerDelay variable):

  • get the latest sensor readings: call the getSensorReadings() function;
  • update the RGB LED colors to match the temperature and humidity values: call the updateColors() functions;
  • send an event to the browser with the latest sensor readings in JSON format.
if (millis() - lastTime > timerDelay) {
  getSensorReadings();
  updateColors();
      
  String message = getJSONReadings();
  events.send(message.c_str(),"new_readings" ,millis());
  lastTime = millis();
}

Demonstration

After successfully uploading all files, you can open the Serial Monitor. If it is running the code for the first time, it will try to read the ssid.txt, pass.txt, and ip.txt files and it won’t succeed because those files weren’t created yet. So, it will start an Access Point.

Serial Monitor ESP32 Access Point Mode

All the LEDs on the shield will be lit in red, indicating that the board is set as an access point.

ESP32 Status Indicator Sensor PCB Shield Access Point Mode

On your computer or smartphone, go to your network settings and connect to the ESP-WIFI-MANAGER access point.

ESP32 board Wi-Fi Manager Access Point web server SSID password

Then, open your browser and go to 192.168.4.1. The Wi-Fi Manager web page should open.

ESP32 Wi-Fi Manager Web Page web server

Enter your network credentials: SSID and Password and an available IP address on your local network. After that, you’ll be redirected to the following page:

ESP32 connected to station success Wi-Fi Manager

At the same time, the ESP should print the following in the Serial Monitor indicating that the parameters you’ve inserted were successfully saved on the corresponding files:

ESP32 Wi-Fi Manager Web Page web server Arduino IDE serial monitor demonstration

After a few seconds, the ESP will restart. And if you’ve inserted the right SSID and password it will start in station mode:

ESP32 Wi-Fi Manager Web Page web server Arduino IDE serial monitor demonstration

All the LEDs on the shield will be lit in a teal color for 30 seconds.

ESP32 Status Indicator Sensor PCB Shield Station Mode

This time, open a browser on your local network and insert the ESP IP address. You should get access to the web page that displays the sensor readings:

ESP32 BME280 Sensor Readings Web Server Table

The LEDs on the shield will light up accordingly to the temperature and humidity range.

ESP32 Status Indicator Sensor PCB Shield Station Mode temperature humidity

Note: the previous picture and the web server print screen were taken at different times (that’s why the values on the table don’t match the number of lit LEDs). You can also watch the video demonstration.

Wrapping Up

In this tutorial, you’ve learned how to create a shield for the ESP32 with a BME280 sensor, two rows of addressable RGB LEDs, and a pushbutton. You’ve also learned how to set up a Wi-Fi Manager for your web server projects. With the Wi-Fi Manager, you can easily connect your ESP web servers to different networks without having to hard-code network credentials. You can apply the Wi-Fi Manager to any web server project.

If you like this tutorial, we have other similar projects that include building and designing PCBs:

Learn more about the ESP32 with our resources:

[Update] the bare PCB giveaway ended and the winners are: Hai, Hasse Lorentzon, Tuan Hazeem, Paul Smulders, and Sudhir Gupta.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

24 thoughts on “ESP32 Neopixel Status Indicator and Sensor PCB Shield with Wi-Fi Manager”

  1. Very useful.
    Purists might be tempted to use one pin for both neopixel strops, but as there are pins enough using separate pins makes the programming easier

    Reply
  2. Thank you so much Rui.
    What an awesome project.
    Please help and modify your project with the ttgo esp32 sim800l to connect to the telegram with it’s gprs instead of Wi-Fi.

    Reply
  3. Thank you for your nice tutorials, even if you do not follow them exactly. So it is an incredibly good tool for hobby engineers.

    Reply
  4. Hello Rui & Sara

    I have this code running on an ESP32-WROOM-32, and it configures the WiFi just fine, attemps to load it after a re-start, but goes back into AP mode after the restart. If I reset the controller, it will connect to my Router after a single reset, then if I reset again, it will not connect, instead going into AP mode again. Then, if I re-set again, it will connect, reset again, AP Mode, ETC.
    With every alternate reset, it connects to the configured WiFi, every other reset, it will go into AP Mode.
    I tried a sketch from another site, which stores the WiFi credentials in EEPROM, rather than SPIFFS, and it exibited the same behavior of connecting every second reset.

    Any idea what is happening?

    Reply
  5. Well, something so simple, Thanks Sara, it is indeed a faulty ESP32! I have one at work, & one at home, that are both set up on breadboards, exactly the same. (I work on the same project, at work or at home, & have the same hardware available for testing) The one at work exibits the issue with the WiFi only working every other reset, while the one at home works as expected.

    Reply
  6. Hi Sara,

    The WS2812B & Serial Monitor display the correct readings, but when I go to the IP ‘192.168.1.200’, I just get a skeleton web page with no readings or time information. Have tried everything I can think of to cure this without success.

    Please help.

    Keep up the great work.

    Alan

    Reply
  7. Hi guys,
    Been following your page for a few months now. Very interesting stuff, everything I’ve tried works beautifully. So a big thank you for all of this.
    While browsing this project, I noticed that the link to the BME module is pointing to a display module. Would be interesting to see where you guys get yours from.
    Thanks!

    Reply
  8. Hi, Hope you are well
    I have problems to find the ESP32 with 36 pin layout do you have a source where to get them?
    Best regards

    Reply
    • Hi.
      This project can work with the ESP8266, but you need to make a few modifications to the code.
      Additionally, if you want to use the shield, you need to redesign it.
      Regards,
      Sara

      Reply
      • I don’t have the experience to develop this idea. case about a little of your time, could make a post about it. Thanks for the answer

        Reply

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.