Table of Contents

Lab 3: Web servers

In this project we'll program the ESP32 Sparrow board to act as a web server on your local network and deliver a simple web page where you can control the on-board RGB led. The web server is mobile responsive and can be accessed with any device that can open a browser in the local network.

So, as a general outline and plan of what you will achieve during this lesson:

Run this example code on your board, but replace the SSID and PASSWORD fields with your own.

webserver.ino
// Load Wi-Fi library
#include <WiFi.h>
 
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
 
// Set web server port number to 80
WiFiServer server(80);
 
// Variable to store the HTTP request
String header;
 
// Auxiliar variables to store the current output state
String output25State = "off";
String output26State = "off";
String output27State = "off";
 
// Assign output variables to GPIO pins
const int output25 = 25;
const int output26 = 26;
const int output27 = 27;
 
// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0; 
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;
 
void setup() {
  Serial.begin(115200);
  // Initialize the output variables as outputs
  pinMode(output25, OUTPUT);
  pinMode(output26, OUTPUT);
  pinMode(output27, OUTPUT);
  // Set outputs to HIGH
  digitalWrite(output25, HIGH);
  digitalWrite(output26, HIGH);
  digitalWrite(output27, HIGH);
 
  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin();
}
 
void loop(){
  WiFiClient client = server.available();   // Listen for incoming clients
 
  if (client) {                             // If a new client connects,
    currentTime = millis();
    previousTime = currentTime;
    Serial.println("New Client.");          // print a message out in the serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected() && currentTime - previousTime <= timeoutTime) {  // loop while the client's connected
      currentTime = millis();
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
 
            // turns the GPIOs on and off
            if (header.indexOf("GET /26/on") >= 0) {
              Serial.println("GPIO 26 on");
              output26State = "on";
              digitalWrite(output26, LOW);
            } else if (header.indexOf("GET /26/off") >= 0) {
              Serial.println("GPIO 26 off");
              output26State = "off";
              digitalWrite(output26, HIGH);
            } else if (header.indexOf("GET /27/on") >= 0) {
              Serial.println("GPIO 27 on");
              output27State = "on";
              digitalWrite(output27, LOW);
            } else if (header.indexOf("GET /27/off") >= 0) {
              Serial.println("GPIO 27 off");
              output27State = "off";
              digitalWrite(output27, HIGH);
            } else if (header.indexOf("GET /25/on") >= 0) {
              Serial.println("GPIO 25 on");
              output25State = "on";
              digitalWrite(output25, LOW);
            } else if (header.indexOf("GET /25/off") >= 0) {
              Serial.println("GPIO 25 off");
              output25State = "off";
              digitalWrite(output25, HIGH);
            }
 
            // Display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // CSS to style the on/off buttons 
            // Feel free to change the background-color and font-size attributes to fit your preferences
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 {background-color: #555555;}");
            client.println(".button3 {background-color: #555555;}</style></head>");
 
            // Web Page Heading
            client.println("<body><h1>ESP32 Web Server</h1>");
 
            // Display current state, and ON/OFF buttons for GPIO 56  
            client.println("<p>GPIO 25 - State " + output25State + "</p>");
            // If the output25State is off, it displays the ON button       
            if (output25State=="off") {
              client.println("<p><a href=\"/25/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/25/off\"><button class=\"button button2 button3\">OFF</button></a></p>");
            } 
 
            // Display current state, and ON/OFF buttons for GPIO 26  
            client.println("<p>GPIO 26 - State " + output26State + "</p>");
            // If the output26State is off, it displays the ON button       
            if (output26State=="off") {
              client.println("<p><a href=\"/26/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/26/off\"><button class=\"button button2 button3\">OFF</button></a></p>");
            } 
 
            // Display current state, and ON/OFF buttons for GPIO 27  
            client.println("<p>GPIO 27 - State " + output27State + "</p>");
            // If the output27State is off, it displays the ON button       
            if (output27State=="off") {
              client.println("<p><a href=\"/27/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/27/off\"><button class=\"button button2 button3\">OFF</button></a></p>");
            }
            client.println("</body></html>");
 
            // The HTTP response ends with another blank line
            client.println();
            // Break out of the while loop
            break;
          } else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}

Test the code works in your phone's or computer's browser and switch on/off the LEDs.

Assignment

Modify the code to control the RGB led in all its colors from the web browser. You will need to replace each button with a text box in which you will input the brightness value for each of the three channels. The brightness needs to be an integer from 0 to 255. Use the analogWrite(pin, value) function to control the PWM channels on the GPIO pins.

SPIFFS Webserver

The previous web server example works well, but it's not a good practice to hard-code HTML directly into C code, as it makes things hard to debug and doesn't scale well if we want to create more elaborate web pages. Luckily we can use the 4MB on-board Flash memory of the ESP32 module to store separate files and access them through the SPIFFS.

You will need to add a plugin to your Arduino IDE (at this moment works only with the 1.x versions of the IDE). Please use this tutorial to install the plugin.

Async Web Server Libraries

From now on, we will build web servers that use the following libraries: ESPAsyncWebServer and AsyncTCP.

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.

RGB Color Picker

Now, let's build a new webserver that shows a slider control which you can use to change the color of one of the on-board leds.

This is how the project should work:

First step is to download the sketch below and save it in a folder named “webserver”.

webserver.ino
// Import required libraries
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
 
// Replace with your network credentials
const char* ssid     = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
 
const int output = 14; //red led
 
String sliderValue = "0";
 
// setting PWM properties
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;
 
const char* PARAM_INPUT = "value";
 
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
 
// Replaces placeholder with button section in your web page
String processor(const String& var){
  Serial.println(var);
  /*if (var == "SLIDERVALUE"){
    return sliderValue;
  }*/
  return String();
}
 
void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
 
   // Initialize SPIFFS
  if(!SPIFFS.begin(true)){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  // configure LED PWM functionalitites
  ledcSetup(ledChannel, freq, resolution);
 
  // attach the channel to the GPIO to be controlled
  ledcAttachPin(output, ledChannel);
 
  ledcWrite(ledChannel, sliderValue.toInt());
 
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
 
  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());
 
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
     request->send(SPIFFS, "/index.html", String(), false, processor);
 
  });
 
  // Send a GET request to <ESP_IP>/slider?value=<inputMessage>
  server.on("/slider_r", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    // GET input1 value on <ESP_IP>/slider?value=<inputMessage>
    if (request->hasParam(PARAM_INPUT)) {
      inputMessage = request->getParam(PARAM_INPUT)->value();
      sliderValue = inputMessage;
      Serial.println(sliderValue.toInt());
      ledcWrite(ledChannel, 255 - sliderValue.toInt());
    }
    else {
      inputMessage = "No message sent";
    }
 
    request->send(200, "text/plain", "OK");
  });
 
  // Start server
  server.begin();
}
 
void loop() {
 
}

This is the index.html file that the webserver will send anytime a client connects to it. Download the file and save it in your current sketch folder in a sub-folder named “data”. So, the path to it should be (..)/webserver/data/index.html

index.html
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ESP Web Server</title>
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 2.3rem;}
    p {font-size: 1.9rem;}
    body {max-width: 400px; margin:0px auto; padding-bottom: 25px;}
    .slider { -webkit-appearance: none; margin: 14px; width: 360px; height: 25px; background: #FFD65C;
      outline: none; -webkit-transition: .2s; transition: opacity .2s;}
    .slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
    .slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; } 
  </style>
</head>
<body>
  <h1>ESP32 Web Server</h1>
  <h3>Red Channel</h3>
  <p><span id="textSliderValueR">%SLIDERVALUE_R%</span></p>
  <p><input type="range" onchange="updateSliderR(this)" id="pwmSlider_r" min="0" max="255" value="%SLIDERVALUE_R%" step="1" class="slider"></p>
  <h3>Green Channel</h3>
  <p><span id="textSliderValueG">%SLIDERVALUE_G%</span></p>
  <p><input type="range" onchange="updateSliderG(this)" id="pwmSlider_g" min="0" max="255" value="%SLIDERVALUE_G%" step="1" class="slider"></p>
  <h3>Blue Channel</h3>
  <p><span id="textSliderValueB">%SLIDERVALUE_B%</span></p>
  <p><input type="range" onchange="updateSliderB(this)" id="pwmSlider_b" min="0" max="255" value="%SLIDERVALUE_B%" step="1" class="slider"></p>
<script>
function updateSliderR(element) {
  var sliderValue = document.getElementById("pwmSlider_r").value;
  document.getElementById("textSliderValueR").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider_r?value="+sliderValue, true);
  xhr.send();
}
function updateSliderG(element) {
  var sliderValue = document.getElementById("pwmSlider_g").value;
  document.getElementById("textSliderValueG").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider_g?value="+sliderValue, true);
  xhr.send();
}
function updateSliderB(element) {
  var sliderValue = document.getElementById("pwmSlider_b").value;
  document.getElementById("textSliderValueB").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider_b?value="+sliderValue, true);
  xhr.send();
}
</script>
</body>
</html>
Assignment

Modify the webserver sketch to receive data from the other two sliders in the webpage and control the other two PWM channels (13 - green and 15 - blue).

Live charts

 Light plot displayed automatically by the server

Next, we'll use the data provided by the on-board sensors to generate dynamic web pages that plot sensor reading in real-time.

We will use the LTR308 light sensor to supply the light intensity values that we would want to display in the web page in a plot.

The way this works is similar to the previous examples:

webserver.ino
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <LTR308.h>
#include <Wire.h>
 
LTR308 light;
unsigned char ID;
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
 
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
 
void initLight(){
  light.begin();
  // To start taking measurements, power up the sensor
 
  light.setPowerUp();
 
  // Allow for a slight delay in power-up sequence (typ. 5ms from the datasheet)
  delay(10);
  light.setGain(0);
  light.setMeasurementRate(0, 3);
  }
 
String readLight(){
  unsigned long rawData;
  double lux;    // Resulting lux value
  boolean good;  // True if sensor is not saturated
 
  light.getData(rawData);
  good = light.getLux(0, 0, rawData, lux);
  if(good) 
    return String(lux);
  else { 
    Serial.println("Failed to read light sensor!");
    return "-1";
  }
 }
 
 
void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
 
  light.begin();
  if (light.getPartID(ID)) {
    Serial.print("Got Sensor Part ID: 0X");
    Serial.print(ID, HEX);
    Serial.println();
  }
  else {
    Serial.println("Could not initialize LTR308 sensor");
    while(1);
  }
  initLight();
 
  // Initialize SPIFFS
  if(!SPIFFS.begin()){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
 
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
 
  // Print ESP32 Local IP Address
  Serial.println(WiFi.localIP());
 
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html");
  });
  server.on("/light", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", readLight().c_str());
  });
 
  // Start server
  server.begin();
}
 
void loop(){
 
}
index.html
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://code.highcharts.com/highcharts.js"></script>
  <style>
    body {
      min-width: 310px;
    	max-width: 800px;
    	height: 400px;
      margin: 0 auto;
    }
    h2 {
      font-family: Arial;
      font-size: 2.5rem;
      text-align: center;
    }
  </style>
</head>
<body>
  <h2>ESP Weather Station</h2>
  <div id="chart-light" class="container"></div>
</body>
<script>
var chartL = new Highcharts.Chart({
  chart:{ renderTo : 'chart-light' },
  title: { text: 'LTR308 Light Intensity' },
  series: [{
    showInLegend: false,
    data: []
  }],
  plotOptions: {
    line: { animation: false,
      dataLabels: { enabled: true }
    },
    series: { color: '#059e8a' }
  },
  xAxis: { type: 'datetime',
    dateTimeLabelFormats: { second: '%H:%M:%S' }
  },
  yAxis: {
    title: { text: 'Illumination (lux)' }
  },
  credits: { enabled: false }
});
setInterval(function ( ) {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var x = (new Date()).getTime(),
          y = parseFloat(this.responseText);
      //console.log(this.responseText);
      if(chartL.series[0].data.length > 40) {
        chartL.series[0].addPoint([x, y], true, true, true);
      } else {
        chartL.series[0].addPoint([x, y], true, false, true);
      }
    }
  };
  xhttp.open("GET", "/light", true);
  xhttp.send();
}, 3000 ) ;
 
</script>
</html>
Assignment

Modify the above example to add temperature, humidity and pressure readings from the BME680 sensor.