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.
// 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.
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.
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.
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”.
// 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
<!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>
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:
#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(){ }
<!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>