ESP32 IoT Shield PCB with Dashboard for Outputs and Sensors

In this project we’ll show you how to build an IoT shield PCB for the ESP32 and a web server dashboard to control it. The shield is equipped with a BME280 sensor (temperature, humidity and pressure), an LDR (light dependent resistor), a PIR motion sensor, a status LED, a pushbutton and a terminal socket to connect a relay module or any other output.

ESP32 IoT Shield PCB with Dashboard for Outputs and Sensors

Alternatively, you can also follow this project by wiring the circuit on a breadboard.

Watch the Video Tutorial

This project is available in video format and in written format. You can watch the video below or you can scroll down for the written instructions.

Resources

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

Project Overview

This project consists of two parts:

  1. Designing and Building the IoT shield
  2. Programming the IoT shield using Arduino IDE

IoT Shield Features

The IoT sensor shield is designed to be stacked to the ESP32. For this reason, if you want to use our PCB, you need the same ESP32 board. We’re using the ESP32 DEVKIT DOIT V1 board (the model with 36 GPIOs).

ESP32 IoT Sensor Shield PCB and ESP32 board

If you have another ESP32 model, you can still follow this project by assembling the circuit on a breadboard or modifying the PCB layout and wiring to match your ESP32 board.

ESP32 IoT Sensor Shield circuit assembled on Breadboard

The shield consists of:

  • BME280 temperature, humidity and pressure sensor;
  • LDR (light dependent resistor);
  • PIR motion sensor;
  • Status on-board LED;
  • Pushbutton;
  • 3-pin socket that gives you access to GND, 5V and a GPIO where you can connect any output (like a relay module for example).

ESP32 IoT Shield Pin Assignment

The following table describes the pin assignment for each component of the IoT shield:

ComponentESP32 Pin Assignment
BME280GPIO 21 (SDA), GPIO 22 (SCL)
PIR Motion SensorGPIO 27
Light Dependent Resistor (LDR)GPIO 33
PushbuttonGPIO 18
LEDGPIO 19
Additional OutputGPIO 32

If you want to assign and use different pins, read our ESP32 Pinout Reference Guide.

Web Server (IoT Dashboard) Features

To control the shield, we’ll build a web server. However, you can program the sensor shield as you wish with any other web server or to integrate it with a home automation platform.

Web Server IoT Dashboard Features overview

Here’s the web server features to control the IoT shield:

  • To access the web server, you need to login with username and password (read: ESP32 Web Server HTTP Authentication: Username and Password Protected).
  • After authenticating with the right credentials, you can access the web server. There’s an icon at the top of the web page that you can click to logout. Then, you’ll need to login again.
  • There are two toggle switches: one to control the output socket and another for the on-board status LED.
  • The status LED can also be controlled using the physical on-shield pushbutton. The state of the LED automatically updates on the web page (like in this tutorial: Control Outputs with Web Server and a Physical Button Simultaneously). The toggle switch for the status LED can be useful to activate or deactivate something on the ESP32 and the LED gives you a visual feedback of what’s going on.
  • The temperature, humidity and luminosity are displayed on the web server and are automatically updated using server-sent events (SSE).
  • Finally, there’s a card that indicates if motion was detected. After receiving the “Motion Detected” notification, you can click on the card to clear the warning.

These are the main features of the ESP32 IoT dashboard we’re going to build. This combines many of the subjects approached in previous tutorials.

This is just an example on how you can control your shield. The idea is to modify the code to add your own features to the project.

Testing the Circuit on a Breadboard

Before designing and building the PCB shield, 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.

ESP32 IoT Sensor Shield circuit assembled on Breadboard

Parts Required

To assemble the circuit on a breadboard 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!

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

Testing the Circuit on a Breadboard ESP32 IoT PCB Shield

Designing the PCB

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

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.

ESP32 IoT Shield PCB Schematic Circuit Diagram

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

ESP32 IoT Shield PCB Schematic footprints

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 to download the file.

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

PCBWay Order PCB import zip folder with Gerber files

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 fill 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.

PCBWay Order PCB Gerber files preview window

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. I’ve ordered the Blue color.

PCBWay Order PCB final step and save to cart

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

Unboxing

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

PCBWay Unboxing

Everything comes well packed, and the PCBs are really high-quality. The letters on the silkscreen are really well-printed and easy to read. Additionally, the solder sticks easily to the pads.

PCBWay Unboxing PCBs bare boards

Besides the PCBs, I also received some stickers, a ruler and a pen. Overall, we’re really satisfied with the PCBWay service.

PCBWay Unboxing PCBs

Soldering the Components

The next step is soldering the components to the PCB. I’ve used an SMD LED and SMD resistors. These can be a bit difficult to solder, but they save a lot of space on the PCB.

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

ESP32 IoT Sensor Shield components parts required

Here’s the soldering tools I’ve used:

TS80 Soldering Iron Review Best Portable Soldering Iron

Read our review about the TS80 Soldering Iron: TS80 Soldering Iron Review – Best 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.

ESP32 PCB Shield soldering with TS80 portable soldering iron

Here’s how the ESP32 IoT Shield looks like after assembling all the parts. It should connect perfectly to the ESP32 DEVKIT DOIT V1 board.

ESP32 IoT Shield PCB soldered and assembled

Programming the ESP32 IoT Shield

The code for this project runs a web server that allows you to monitor and control the IoT shield. The features of the web server were covered previously.

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

Installing Libraries

Before uploading the code, make sure you have the following libraries installed:

Follow the next steps to install the libraries:

Open your Arduino IDE and go to Sketch Include Library > Manage Libraries. The Library Manager should open.

Search for “adafruit bme280 ” on the Search box and install the library.

Installing the BME280 library Arduino IDE

To use the BME280 library, you also need to install the Adafruit_Sensor library. Follow the next steps to install the library in your Arduino IDE:

Go to Sketch Include Library > Manage Libraries and type “Adafruit Unified Sensor” in the search box. Scroll all the way down to find the library and install it.

Installing Adafruit Unified Sensor library Arduino IDE

To install the ESPAsyncWebServer and the AsyncTCP libraries, click on the following links to download the .zip folder:

These 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.

Code – ESP32 IoT Shied Web Server Dashboard

Copy the following code to the Arduino IDE.

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-iot-shield-pcb-dashboard/
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

// Import required libraries
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Web Server HTTP Authentication credentials
const char* http_username = "admin";
const char* http_password = "admin";

Adafruit_BME280 bme;         // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
const int buttonPin = 18;    // Pushbutton
const int ledPin = 19;       // Status LED
const int output = 32;       // Output socket
const int ldr = 33;          // LDR (Light Dependent Resistor)
const int motionSensor = 27; // PIR Motion Sensor

int ledState = LOW;           // current state of the output pin
int buttonState;              // current reading from the input pin
int lastButtonState = LOW;    // previous reading from the input pin
bool motionDetected = false;  // flag variable to send motion alert message
bool clearMotionAlert = true; // clear last motion alert message from web page

unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncEventSource events("/events");

const char* PARAM_INPUT_1 = "state";

// Checks if motion was detected
void IRAM_ATTR detectsMovement() {
  //Serial.println("MOTION DETECTED!!!");
  motionDetected = true;
  clearMotionAlert = false;
}

// Main HTML web page in root url /
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP IOT DASHBOARD</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">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h3 {font-size: 1.8rem; color: white;}
    h4 { font-size: 1.2rem;}
    p { font-size: 1.4rem;}
    body {  margin: 0;}
    .switch {position: relative; display: inline-block; width: 120px; height: 68px; margin-bottom: 20px;}
    .switch input {display: none;}
    .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 68px;   opacity: 0.8;   cursor: pointer;}
    .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
    input:checked+.slider {background-color: #1b78e2}
    input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
    .topnav { overflow: hidden; background-color: #1b78e2;}
    .content { padding: 20px;}
    .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);}
    .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));}
    .slider2 { -webkit-appearance: none; margin: 14px;  height: 20px; background: #ccc; outline: none; opacity: 0.8; -webkit-transition: .2s; transition: opacity .2s; margin-bottom: 40px; }
    .slider:hover, .slider2:hover { opacity: 1; }
    .slider2::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 40px; height: 40px; background: #008B74; cursor: pointer; }
    .slider2::-moz-range-thumb { width: 40px; height: 40px; background: #008B74; cursor: pointer;}
    .reading { font-size: 2.6rem;}
    .card-switch {color: #50a2ff; }
    .card-light{ color: #008B74;}
    .card-bme{ color: #572dfb;}
    .card-motion{ color: #3b3b3b; cursor: pointer;}
    .icon-pointer{ cursor: pointer;}
  </style>
</head>
<body>
  <div class="topnav">
    <h3>ESP IOT DASHBOARD <span style="text-align:right;">&nbsp;&nbsp; <i class="fas fa-user-slash icon-pointer" onclick="logoutButton()"></i></span></h3>
  </div>
  <div class="content">
    <div class="cards">
      %BUTTONPLACEHOLDER%
      <div class="card card-bme">
        <h4><i class="fas fa-chart-bar"></i> TEMPERATURE</h4><div><p class="reading"><span id="temp"></span>&deg;C</p></div>
      </div>
      <div class="card card-bme">
        <h4><i class="fas fa-chart-bar"></i> HUMIDITY</h4><div><p class="reading"><span id="humi"></span>&percnt;</p></div>
      </div>
      <div class="card card-light">
        <h4><i class="fas fa-sun"></i> LIGHT</h4><div><p class="reading"><span id="light"></span></p></div>
      </div>
      <div class="card card-motion" onClick="clearMotionAlert()">
        <h4><i class="fas fa-running"></i> MOTION SENSOR</h4><div><p class="reading"><span id="motion">%MOTIONMESSAGE%</span></p></div>
      </div>
  </div>
<script>
function logoutButton() {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/logout", true);
  xhr.send();
  setTimeout(function(){ window.open("/logged-out","_self"); }, 1000);
}
function controlOutput(element) {
  var xhr = new XMLHttpRequest();
  if(element.checked){ xhr.open("GET", "/output?state=1", true); }
  else { xhr.open("GET", "/output?state=0", true); }
  xhr.send();
}
function toggleLed(element) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/toggle", true);
  xhr.send();
}
function clearMotionAlert() {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/clear-motion", true);
  xhr.send();
  setTimeout(function(){
    document.getElementById("motion").innerHTML = "No motion";
    document.getElementById("motion").style.color = "#3b3b3b";
  }, 1000);
}
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('message', function(e) {
  console.log("message", e.data);
 }, false);
 source.addEventListener('led_state', function(e) {
  console.log("led_state", e.data);
  var inputChecked;
  if( e.data == 1){ inputChecked = true; }
  else { inputChecked = false; }
  document.getElementById("led").checked = inputChecked;
 }, false);
 source.addEventListener('motion', function(e) {
  console.log("motion", e.data);
  document.getElementById("motion").innerHTML = e.data;
  document.getElementById("motion").style.color = "#b30000";
 }, false); 
 source.addEventListener('temperature', function(e) {
  console.log("temperature", e.data);
  document.getElementById("temp").innerHTML = e.data;
 }, false);
 source.addEventListener('humidity', function(e) {
  console.log("humidity", e.data);
  document.getElementById("humi").innerHTML = e.data;
 }, false);
 source.addEventListener('light', function(e) {
  console.log("light", e.data);
  document.getElementById("light").innerHTML = e.data;
 }, false);
}</script>
</body>
</html>)rawliteral";

String outputState(int gpio){
  if(digitalRead(gpio)){
    return "checked";
  }
  else {
    return "";
  }
}

String processor(const String& var){
  //Serial.println(var);
  if(var == "BUTTONPLACEHOLDER"){
    String buttons;
    String outputStateValue = outputState(32);
    buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> OUTPUT</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"controlOutput(this)\" id=\"output\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
    outputStateValue = outputState(19);
    buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> STATUS LED</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleLed(this)\" id=\"led\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
    return buttons;
  }
  else if(var == "MOTIONMESSAGE"){
    if(!clearMotionAlert) {
      return String("<span style=\"color:#b30000;\">MOTION DETECTED!</span>");
    }
    else {
      return String("No motion");
    }
  }
  return String();
}

// Logged out web page
const char logout_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <p>Logged out or <a href="/">return to homepage</a>.</p>
  <p><strong>Note:</strong> close all web browser tabs to complete the logout process.</p>
</body>
</html>
)rawliteral";

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
    
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
  
  // initialize the pushbutton pin as an input
  pinMode(buttonPin, INPUT);
  // initialize the LED pin as an output
  pinMode(ledPin, OUTPUT);
  // initialize the LED pin as an output
  pinMode(output, OUTPUT);
  // PIR Motion Sensor mode INPUT_PULLUP
  pinMode(motionSensor, INPUT_PULLUP);
  // Set motionSensor pin as interrupt, assign interrupt function and set RISING mode
  attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);
  
  // 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){
   if(!request->authenticate(http_username, http_password))
      return request->requestAuthentication();
    request->send_P(200, "text/html", index_html, processor);
  });
  server.on("/logged-out", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", logout_html, processor);
  });
  server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(401);
  });
  // Send a GET request to control output socket <ESP_IP>/output?state=<inputMessage>
  server.on("/output", HTTP_GET, [] (AsyncWebServerRequest *request) {
    if(!request->authenticate(http_username, http_password))
      return request->requestAuthentication();
    String inputMessage;
    // GET gpio and state value
    if (request->hasParam(PARAM_INPUT_1)) {
      inputMessage = request->getParam(PARAM_INPUT_1)->value();
      digitalWrite(output, inputMessage.toInt());
      request->send(200, "text/plain", "OK");
    }
    request->send(200, "text/plain", "Failed");
  });
  // Send a GET request to control on board status LED <ESP_IP>/toggle
  server.on("/toggle", HTTP_GET, [] (AsyncWebServerRequest *request) {
    if(!request->authenticate(http_username, http_password))
      return request->requestAuthentication();
    ledState = !ledState;
    digitalWrite(ledPin, ledState);
    request->send(200, "text/plain", "OK");
  });
  // Send a GET request to clear the "Motion Detected" message <ESP_IP>/clear-motion
  server.on("/clear-motion", HTTP_GET, [] (AsyncWebServerRequest *request) {
    if(!request->authenticate(http_username, http_password))
      return request->requestAuthentication();
    clearMotionAlert = true;
    request->send(200, "text/plain", "OK");
  });
  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis and set reconnect delay to 1 second
    client->send("hello!",NULL,millis(),1000);
  });
  server.addHandler(&events);
  
  // Start server
  server.begin();
}
 
void loop(){
  static unsigned long lastEventTime = millis();
  static const unsigned long EVENT_INTERVAL_MS = 10000;
  // read the state of the switch into a local variable
  int reading = digitalRead(buttonPin);

  // If the switch changed
  if (reading != lastButtonState) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    // if the button state has changed:
    if (reading != buttonState) {
      buttonState = reading;
      // only toggle the LED if the new button state is HIGH
      if (buttonState == HIGH) {
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
        events.send(String(digitalRead(ledPin)).c_str(),"led_state",millis());
      }
    }
  }

  if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
    events.send("ping",NULL,millis());
    events.send(String(bme.readTemperature()).c_str(),"temperature",millis());
    events.send(String(bme.readHumidity()).c_str(),"humidity",millis());
    events.send(String(analogRead(ldr)).c_str(),"light",millis());
    lastEventTime = millis();
  }
  
  if(motionDetected & !clearMotionAlert){
    events.send(String("MOTION DETECTED!").c_str(),"motion",millis());
    motionDetected = false;
  }
  
  // save the reading. Next time through the loop, it'll be the lastButtonState:
  lastButtonState = reading;
}

View raw code

This code is quite long to explain, so you can simply replace the following two variables with your network credentials and the code will work straight away.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

If you want to learn how this code works, continue reading. Otherwise, you can skip to the Demonstration section.

How the Code Works

Read this section if you want to learn how the code works, or skip to the next section.

The following lines import the required libraries:

#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

Insert your network credentials in the following lines so that the ESP32 can connect to your network.

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

The next lines define the username and password to access the web server. By default the username is admin and the password is admin. You can change them on the following lines:

// Web Server HTTP Authentication credentials
const char* http_username = "admin";
const char* http_password = "admin";

Create an Adafruit_BME280 object called bme. This creates an I2C connection to the BME280 on GPIO 21 and GPIO 22.

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

Then, define the GPIOs the components of the shield are connected to.

const int buttonPin = 18;    // Pushbutton
const int ledPin = 19;       // Status LED
const int output = 32;       // Output socket
const int ldr = 33;          // LDR (Light Dependent Resistor)
const int motionSensor = 27; // PIR Motion Sensor

Create the following variables to old states. The comments explain what each variable means.

int ledState = LOW;           // current state of the output pin
int buttonState;              // current reading from the input pin
int lastButtonState = LOW;    // previous reading from the input pin
bool motionDetected = false;  // flag variable to send motion alert message
bool clearMotionAlert = true; // clear last motion alert message from web page

The lastDebounceTime and the debounceDelay variables are used to debounce the button. This prevents false positive button presses.

unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 50;   // the debounce time; increase if the output flickers

Create an AsyncWebServer object on port 80.

AsyncWebServer server(80);

To automatically display the information on the web server when new readings are available, we’ll use Server-Sent Events (SSE).

The following line creates a new event source on /events. Server-Sent Events allow a web page (client) to get updates from a server.

AsyncEventSource events("/events");

The PARAM_INPUT_1 variable will be used to check whether a certain URL request contains the parameter “state“.

const char* PARAM_INPUT_1 = "state";

Interrupt Callback Function

The detectsMovement() callback function will be called when the PIR motion sensor senses motion (an interrupt is triggered). The function changes the state of the motionDetected variable to true so that we know that motion was detected and set the clearMotionAlert variable to false because we want the “Motion Detected” message to be displayed on the web server.

void IRAM_ATTR detectsMovement() {
  //Serial.println("MOTION DETECTED!!!");
  motionDetected = true;
  clearMotionAlert = false;
}

Building the Web Page

The index_html variable contains all the HTML, CSS and JavaScript to build the web page. We won’t go into details on how the HTML and CSS works. We’ll just take a look at how to handle the events sent by the server.

Handle 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 here 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);

source.addEventListener('message', function(e) {
 console.log("message", e.data);
}, false);

Then, add the other event listeners.

When you change the status LED state, the ESP32 sends an event (led_state) with that information so that the dashboard updates automatically.

source.addEventListener('led_state', function(e) {
  console.log("led_state", e.data);
  var inputChecked;
  if( e.data == 1){ inputChecked = true; }
  else { inputChecked = false; }
  document.getElementById("led").checked = inputChecked;
}, false);

When the browser receives this event, it changes the state of the toggle switch element.

The motion event is sent when motion is detected. When this happens, it changes the content of the message and changes its color.

source.addEventListener('motion', function(e) {
  console.log("motion", e.data);
  document.getElementById("motion").innerHTML = e.data;
  document.getElementById("motion").style.color = "#b30000";
}, false); 

The temperature, humidity and light events are sent to the browser when new readings are available.

source.addEventListener('temperature', function(e) {
  console.log("temperature", e.data);
  document.getElementById("temp").innerHTML = e.data;
}, false);
source.addEventListener('humidity', function(e) {
  console.log("humidity", e.data);
  document.getElementById("humi").innerHTML = e.data;
}, false);
source.addEventListener('light', function(e) {
  console.log("light", e.data);
  document.getElementById("light").innerHTML = e.data;
}, false);

When that happens, we put the received data into the elements with the corresponding id.

outputState() function

The outputState() function is used to check the current output state of a GPIO. It returns “checked” if the GPIO is on or an empty string if it isn’t. The returned string will be used to build the web page with the current outputs states. This way, every time you access the web server you see the current states.

String outputState(int gpio){
  if(digitalRead(gpio)){
    return "checked";
  }
  else {
    return "";
  }
}

processor()

The processor() function replaces the placeholders on the HTML text with whatever string we want. We use the processor() function so that when you access the web server page for the first time in a new browser tab, it shows the current GPIO states, and motion sensor state.

The BUTTONPLACEHODER is replaced with the HTML text to build the button with the right states.

if(var == "BUTTONPLACEHOLDER"){
  String buttons;
  String outputStateValue = outputState(32);
  buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> OUTPUT</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"controlOutput(this)\" id=\"output\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
  outputStateValue = outputState(19);
  buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> STATUS LED</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleLed(this)\" id=\"led\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
  return buttons;
}

The MOTIONMESSAGE placeholder is replaced with the MOTION DETECTED message or No motion message, depending on the current motion state.

else if(var == "MOTIONMESSAGE"){
  if(!clearMotionAlert) {
    return String("<span style=\"color:#b30000;\">MOTION DETECTED!</span>");
  }
  else {
    return String("No motion");
  }
}
return String();

Logout Page

The logout_html variable contains the HTML text to build the logout page. You are redirected to the logout page when you click on the web page logout button.

const char logout_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <p>Logged out or <a href="/">return to homepage</a>.</p>
  <p><strong>Note:</strong> close all web browser tabs to complete the logout process.</p>
</body>
</html>
)rawliteral";

In the logout page, there’s a link that allows you to go back to the login page (root / URL).

<p>Logged out or <a href="/">return to homepage</a>.</p>

setup()

In the setup(), initialize the serial monitor.

Serial.begin(115200);

Initialize the BME280 sensor.

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

Set the button as an input, the status led and the additional output as outputs and the PIR motion sensor as an interrupt.

// initialize the pushbutton pin as an input
pinMode(buttonPin, INPUT);
// initialize the LED pin as an output
pinMode(ledPin, OUTPUT);
// initialize the LED pin as an output
pinMode(output, OUTPUT);
// PIR Motion Sensor mode INPUT_PULLUP
pinMode(motionSensor, INPUT_PULLUP);
// Set motionSensor pin as interrupt, assign interrupt function and set RISING mode
attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

Connect to wi-fi and print the ESP32 IP address.

// 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());

Handle Requests

We need to handle what happens when the ESP32 receives a request on a certain URL.

Handle Requests with Authentication

Every time you make a request to the ESP32 to access the web server, it will check whether you’ve already entered the correct username and password to authenticate.

Basically, to add authentication to your web server, you just need to add the following lines after each request:

if(!request->authenticate(http_username, http_password))
  return request->requestAuthentication();

These lines continuously pop up the authentication window until you insert the right credentials.

You need to do this for all requests. This way, you ensure that you’ll only get responses if you are logged in.

For example, when you try to access the root URL (ESP IP address), you add the previous two lines before sending the page. If you enter the wrong credentials, the browser will keep asking for them.

Recommended reading: ESP32/ESP8266 Web Server HTTP Authentication (Username and Password Protected)

If you access the root / URL and insert the right credentials, send the main web page (saved on the index_html) variable.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
 if(!request->authenticate(http_username, http_password))
    return request->requestAuthentication();
  request->send_P(200, "text/html", index_html, processor);
});

Handle Logout

When you click the logout button, the ESP receives a request on the /logout URL. When that happens send the response code 401.

server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(401);
});

The response code 401 is an unauthorized error HTTP response status code indicating that the request sent by the client could not be authenticated. So, it will have the same effect as a logout – it will ask for the username and password and won’t let you access the web server again until you login.

When you click the web server logout button, after one second, the ESP receives another request on the /logged-out URL. When that happens, send the HTML text to build the logout page (logout_html variable).

server.on("/logged-out", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", logout_html, processor);
});

Handle Output

When you click the button to control the output, the ESP receives a request like this /output?state=<inputMessage>. The inputMessage can be either 0 or 1 (off or on).

The following lines checker whether the request on the /output URL contains the parameter state. If it does, save the value of the state into the inputMessage variable. Then, control the output GPIO with the value of that message digitalWrite(output, inputMessage.toInt());

// Send a GET request to control output socket <ESP_IP>/output?state=<inputMessage>
server.on("/output", HTTP_GET, [] (AsyncWebServerRequest *request) {
  if(!request->authenticate(http_username, http_password))
    return request->requestAuthentication();
  String inputMessage;
  // GET gpio and state value
  if (request->hasParam(PARAM_INPUT_1)) {
    inputMessage = request->getParam(PARAM_INPUT_1)->value();
    digitalWrite(output, inputMessage.toInt());
    request->send(200, "text/plain", "OK");
  }
  request->send(200, "text/plain", "Failed");
});

Handle Status LED

When you control the status LED, invert the button state.

// Send a GET request to control on board status LED <ESP_IP>/toggle
server.on("/toggle", HTTP_GET, [] (AsyncWebServerRequest *request) {
  if(!request->authenticate(http_username, http_password))
    return request->requestAuthentication();
  ledState = !ledState;
  digitalWrite(ledPin, ledState);
  request->send(200, "text/plain", "OK");
});

Handle Motion

When you click the motion sensor card after motion being detected, you make a request on the /clear-motion URL. When that happens, set the clearMotion variable to true.

server.on("/clear-motion", HTTP_GET, [] (AsyncWebServerRequest *request) {
  if(!request->authenticate(http_username, http_password))
    return request->requestAuthentication();
  clearMotionAlert = true;
  request->send(200, "text/plain", "OK");
});

Server Event Source

Set up the event source 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());
  }
  // send event with message "hello!", id current millis and set reconnect delay to 1 second
  client->send("hello!",NULL,millis(),1000);
});
server.addHandler(&events);

Finally, start the web server.

server.begin();

loop()

In the loop(), check the pushbutton state. If the button state has changed its state, change the output LED state accordingly, and send an event to the browser to change the output state on the web page.

int reading = digitalRead(buttonPin);

// If the switch changed
if (reading != lastButtonState) {
  // reset the debouncing timer
  lastDebounceTime = millis();
}

if ((millis() - lastDebounceTime) > debounceDelay) {
  // if the button state has changed:
  if (reading != buttonState) {
    buttonState = reading;
    // only toggle the LED if the new button state is HIGH
    if (buttonState == HIGH) {
      ledState = !ledState;
      digitalWrite(ledPin, ledState);
      events.send(String(digitalRead(ledPin)).c_str(),"led_state",millis());
    }
  }
}

Send sensor readings to the browser using server-sent events, every 10 seconds. You can change that period of time in the EVENT_INTERVAL_MS variable.

if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
  events.send("ping",NULL,millis());
  events.send(String(bme.readTemperature()).c_str(),"temperature",millis());
  events.send(String(bme.readHumidity()).c_str(),"humidity",millis());
  events.send(String(analogRead(ldr)).c_str(),"light",millis());
  lastEventTime = millis();
}

When motion is detected and if we haven’t cleared the notification, send the MOTION DETECTED message in the event.

if(motionDetected & !clearMotionAlert){
  events.send(String("MOTION DETECTED!").c_str(),"motion",millis());
  motionDetected = false;
}

Upload the Code

To upload code, go to Tools> Board and select DOIT ESP32 DEVKIT V1. Go to Tools > Port and select the COM port the ESP32 is connected to. Then, click the upload button:

Testing the Multisensor Shield

Open the Serial Monitor at a baud rate of 112500. Press the ESP32 RST button to print the ESP IP address.

ESP32 IP Address printed in Arduino IDE Serial monitor

Open your browser and type the ESP32 IP address. The following page should load. Insert the username and password to access the web server. By default the username is admin and the password is admin. You can change that on the code.

ESP32 IOT dashboard web server login http authentication

After inserting the right credentials, you have access to the dashboard functionalities. There are two toggle switches: one to control the status LED and another to control the additional output.

ESP32 IOT dashboard web server control outputs

You can control the status LED using the toggle switch and also the shield physical button. The state is automatically updated on the web page. There’s another toggle button to control an additional output like a relay module.

ESP32 IoT Sensor Shield Breadboard Circuit Demonstration

Recommended reading: ESP32 Relay Module – Control AC Appliances (Web Server)

The web server shows the latest sensor readings. The readings are updated every 10 seconds automatically using server-sent events. This means that when the ESP32 grabs new readings, it sends an event to the client (your browser). When this event happens, it updates the fields with new readings.

ESP32 IoT Dashboard Web Server Display Sensor Readings

Finally, there’s a card indicating if motion was detected or not. When motion is detected, it shows the “Motion Detected” message. This message is also updated automatically using server-sent events.

ESP32 IoT Dashboard Web Server Motion

Once, you’ve seen this notification, you can click the motion card. It will clear the warning message and show “No motion” instead”.

ESP32 IoT Dashboard Web Server No Motion

Wrapping Up

We hope you’ve found this project useful and you’re able to build it yourself. You can program the IoT Shield with other code suitable for your needs. For example, you can control the output based on the current temperature value or add a threshold field. You can also edit the gerber files and add other features to the ESP32 IoT Shield.

We have other similar projects that include building and designing PCBs that you may like:

Learn more about the ESP32 with our resources:


Learn how to program and build projects with the ESP32 and ESP8266 using MicroPython firmware DOWNLOAD »

Learn how to program and build projects with the ESP32 and ESP8266 using MicroPython firmware DOWNLOAD »


Enjoyed this project? Stay updated by subscribing our weekly newsletter!

41 thoughts on “ESP32 IoT Shield PCB with Dashboard for Outputs and Sensors”

  1. Hi, great project ! Is there a direct link to order the PCBs from PCBWAY ? Please keep up your excellent work !

    Reply
  2. All I’ve got to say is, WOW!!!

    You’ve taken this to the next level, documenting the process for designing a shield, getting it manufactured, and at an exceptional price point.

    Unbelievably through and integrated treatment of this project!

    Reply
  3. Nice little project. I’d like to build it myself. Can you or someone of your community deliver one of his pcb for small money?
    I’d prefer normal throuhole resistors. The others I don’t have on stock. But that is no mayor problem. I’ll try to solder normal resistors on the pcb.
    Many thanks for your good work and your help in understanding electronics.
    Tom

    Reply
  4. Hi,
    I don’t seem to be able to view the pcb on the EasyEDA online editor – I have of course downloaded your files and unzipped them and when I open the pcb_pcb json file (82k) in EasyEDA I just see a blank workspace … any thoughts?

    Reply
    • Ah It’s ok – I thought I was seeing the whole workspace but turns out I wasn’t.
      Weird that it displays the board off screen but hey, I got there in the end. Now to edit and put in more sensors and IO’s.
      Nice

      Reply
  5. Hi, I am keen to build this project but can only seem to find 30 or 38 pin ESP32. Anybody know where I can get a 36 pin one from? even the link in the article goes to a 30 pin one?

    Reply
  6. Thank you for making such great projects. All of your tutorials and projects that you give to us for free is very generous and helpful to us that aren’t good at programming and have very limited funds. I hope you make a lot of money from your advertisers. Again thanks and please keep it you.

    Reply
  7. Hello,
    I have a little question.
    On schematic/PCB why there is a track on the terminal 2 of the P1 Header-Female which is going nowhere ?
    Thanks to you and have a nice day

    Reply
      • Thanks for the reponse
        But : On one side the track goes to GPIO 27 but on the other side where does the track go ?
        Adrien 🙂

        Reply
        • Hi Adrien.
          You are right.
          It’s a small error on the PCB design. There’s that extra line that goes nowhere. Nonetheless, the PCB still works fine.
          Thanks for noticing.
          Regards,
          Sara

          Reply
  8. Hi. Hope you are fine, already have holidays? Hope you have, its not working all the time.
    I`m here again with another dificulty again. Programing the same board you have i get a error, dont know what can be, this is the error i get:

    Foram encontradas múltiplas bibliotecas para «Adafruit_Sensor.h»
    Utilizado: C:\Users\paulo\Documents\Arduino\libraries\Adafruit_Sensor-master
    Não utilizado: C:\Users\paulo\Documents\Arduino\libraries\Adafruit_Unified_Sensor
    Não utilizado: C:\Users\paulo\Documents\Arduino\libraries\AllThingsTalk_LTE-M_SDK
    Foram encontradas múltiplas bibliotecas para «Adafruit_BME280.h»
    In file included from C:\Users\paulo\Documents\Arduino\ESP32DASHBOARD_RANDOMNERDS\ESP32DASHBOARD_RANDOMNERDS.ino:5:0:

    Utilizado: C:\Users\paulo\Documents\Arduino\libraries\Adafruit_BME280_Library
    C:\Users\paulo\Documents\Arduino\libraries\ESPAsyncWebServer-master\src/AsyncEventSource.h:25:22: fatal error: AsyncTCP.h: No such file or directory

    Não utilizado: C:\Users\paulo\Documents\Arduino\libraries\AllThingsTalk_LTE-M_SDK
    compilation terminated.

    exit status 1
    Erro ao compilar para a placa DOIT ESP32 DEVKIT V1.

    I tried esp dev kit, esp wrover and i get the error, dont upload, hope you can help me, thanks a lot.

    Reply
    • Hi.
      After uploading the code, open the serial monitor, and make sure you have the right baud rate selected “115200”.
      Then, press the ESP32 on-board RESET button, and it should print something on the Serial Monitor.
      Regards,
      Sara

      Reply
      • Ok Sara it worked and then i get the message couldnt fund a valid BME280, i have an BMP280 not BME280, but i am waiting for 3 new onws at some time, hope soon to test forword to see if there is any more problems and make it work.
        One more time but not enough thank you very much.

        Reply
  9. Hi Sara mean while i wait for the BME280 I changed the scketch to a BMP280 without pressure everything works fine only light that dont change its always at 4095 with no changes, do you have any idea how to fix, thanks, best regards.

    Reply
  10. Sorry again i remove humidity because BMP280 doesnt have but is missing pressure board, sshould be there i think but it isnt, so i have no readings on light, but board is there with the value of 4095 all the time, and no pressure board, hope you can help, thanks again.

    Reply
  11. hi.
    I thought this dashboard had pressure readings but no, isnt it possible to add this feature? I say this because the BME280 has this capability and the dashboard gets more complete than it is (great as all the others you made). Hope you think on this, the shield is working fine with the exception i cant have no readings on GPIO33 for the light sensor, may be its my problem because i have no readings in the weather shield also, i have checked everything on both and cant imegine why i have no readings,
    Thanks one more time for the help and borough you with so many quetions.

    Reply
  12. Hi Sara i dont have readings on light sensor, cheked everything because i ha dad a error on weather shield that reads ok now but here i have no reads and everything its ok i recheked everything, if you have any idea maybe its missing something i cant see.
    Abought the pressure do you think its possible add the feature even we have to add wires.
    Thanks hope you can help me.
    Regards.

    Reply
  13. Hi Sara.
    Now i receive the BME280 the shield works complete only the light sensor reads but oposit way, if i increase light i get the lower value, if i have low light i have higher values. I already turn the pins of the light sensor like if they have polarity and its the same. Do you wonder what can be?
    Thanks a lot, and if possible help me to read the pressure on this shield.
    Thanks again.

    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.