ESP32 WebSocket Server using Arduino IDE – Control GPIOs and Relays

In this tutorial, we will create an ESP32 WebSocket server using the WebSocket communication protocol and the Arduino IDE. The user will be able to control the onboard LED of the ESP32 and can connect a relay to control AC loads. Through the WebSocket communication protocol, several clients can connect to the server concurrently without blocking each other. We will see that, unlike HTTP, a WebSocket server updates the state of the LED to all clients automatically.

In this tutorial, we will create a WebSocket server in two ways:

  1. By using HTML, CSS, and Javascript files as a string in ESP32 code
  2. ESP32 WebSocket server with SPIFFS where we will store HTML, CSS, and Javascript files in ESP32 flash file system
ESP32 WebSocket Server control GPIO and relays remotely

What is a WebSocket?

A WebSocket follows a two-way communication protocol using a TCP connection whereby the client and the server can transfer data at the same time. It uses the handshaking mechanism to ensure that the server is in synchronization with all the clients and a successful connection is established.

It offers an extremely fast response rate by using two-way communication between the client-server as compared to the simple HTTP protocol. Therefore, in this guide, our objective is to show you the responsiveness of a web server created with WebSocket. The server will update all the connected clients (smartphones, tablets, computers, laptops, etc.) instantaneously when the state of the onboard LED will change.

WebSocket communciation Server

The simple WebSocket handshake process is shown in the picture above. Firstly, the client sends an HTTP request to the server. Through an upgraded header, the server gets to know that the request deals with a WebSocket connection. The servers can handle both the HTTP and the WebSocket connections on the same port by sending data to all the clients.

WebSocket Communication Protocol Key Features

  • Two-way communication over a single Transmission Control Protocol (TCP) connection.
  • Real-time client-server communication is achievable.
  • A big step forward in attaining high speed for real-time web technology.

In the following project, we have also created a web server to control the GPIOs of ESP32. But in this project, we have used an HTTP protocol for communication between server and client. But one of the main disadvantages of HTTP for web server projects is that a server cannot notify all connected clients about the state of GPIO pins whenever the state of GPIO is modified by any client or by a user manually through a physical push button. 

ESP32 WebSocket Server Project Overview

We will create a web server based on the WebSocket protocol. It will consist of a title, “ESP32 WebSocket Server,” the state of the GPIO (ON or OFF), and a toggle button. We aim to build a webpage through which the user will click the button to toggle the onboard LED on the ESP32 board. Whenever the LED’s state will be changed, it will automatically get updated on the webpage immediately. This will occur for all the clients who will be connected with the ESP32 (server).

ESP32 WebSocket Server Control Outputs (Arduino IDE) Demo

Working Process

  • First, the user will click the Toggle button.
  • The web server will send the “toggle” request message through the WebSocket communication protocol.
  • Our ESP32 board will receive the “toggle” request and it performs the particular action.
  • The on-board LED will turn on if it was previously off and vice versa. Next, the ESP32 will send the new LED state to all the connected clients with the help of WebSocket.
  • Immediately all the clients will receive the new message and their respective web pages will update to the new current state.
ESP32 WebSocket Server Control Outputs (Arduino IDE)
Working Process

Setting up Arduino IDE

Make sure you have the latest version of Arduino IDE and the ESP32 plugin installed on your computer.

Installing ESPAsyncWebServer Library and Async TCP Library

We will need two additional libraries to build our web server. The ESPAsyncWebServer library will help us in creating our web server easily. With this library, we will set up an asynchronous HTTP server. AsyncTCP is another library that we will be incorporating as it a dependency for the ESPAsyncWebServer library. Both of these libraries are not available in the Arduino library manager so we will have to download and load them in our ESP32 board ourselves.

  • To install ESPAsyncWebServer library for free, click here to download. You will download the library as a .zip folder which you will extract and rename as ‘ESPAsyncWebServer.’ Then, transfer this folder to the installation library folder in your Arduino IDE.
  • To install Async TCP library for free, click here to download. You will download the library as a .zip folder which you will extract and rename as ‘AsyncTCP.’ Then, transfer this folder to the installation library folder in your Arduino IDE.

Likewise, you can also go to Sketch > Include Library > Add .zip Library inside the IDE to add the libraries as well. After installation of the libraries, restart your IDE.

ESP32 WebSocket Server Arduino Sketch

Open your Arduino IDE and go to File > New. A new file will open. Copy the code given below in that file and save it as ‘ESP32 WebSocket Webserver.’

// Import required libraries
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

// Replace network credentials with your own WiFi details
const char* ssid = "PTCL-BB";
const char* password = "44332211";

bool GPIO_State = 0;
const int Led_Pin = 2;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

const char html_page[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP32 Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
  html {
    font-family: New Times Roman;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #612b78;
  }
  .topnav {
    overflow: hidden;
    background-color: #612b78;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto; 
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color:#fa0f0f;  //green
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
  .button:active {
     background-color:#fa0f0f ; 
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#120707;
     font-weight: bold;
   }
  </style>
<title>ESP32 Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
  <div class="topnav">
    <h1>ESP32 WebSocket Server</h1>
  </div>
  <div class="content">
      <h2>ONBOARD LED GPIO2</h2>
       <p><button id="button" class="button">Click to Toggle</button></p>
      <p class="state">State: <span id="state">%STATE%</span></p>
  </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  window.addEventListener('load', onLoad);
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }
  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }
  function onLoad(event) {
    initWebSocket();
    initButton();
  }
  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }
  function toggle(){
    websocket.send('toggle');
  }
</script>
</body>
</html>
)rawliteral";

void notifyClients() {
  ws.textAll(String(GPIO_State));
}

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    if (strcmp((char*)data, "toggle") == 0) {
      GPIO_State = !GPIO_State;
      notifyClients();
    }
  }
}

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
             void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if (GPIO_State){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
}

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);

  pinMode(Led_Pin, OUTPUT);
  digitalWrite(Led_Pin, LOW);
  
  // 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());

  initWebSocket();

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", html_page, processor);
  });

  // Start server
  server.begin();
}

void loop() {
  ws.cleanupClients();
  digitalWrite(Led_Pin, GPIO_State);
}

How the Code Works?

Importing Libraries

Firstly, we will import the necessary libraries. For this project, we are using three of them. WiFi.h, ESPAsyncWebServer.h and AsyncTCP.h. As we have to connect our ESP32 to a wireless network hence we need WiFi.h library for that purpose. The other two libraries are the ones that we recently downloaded and will be required for the building of the asynchronous web server.

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

Setting Network Credentials

Next, we will create two global variables, one for the SSID and the other for the password. These will hold our network credentials which will be used to connect to our wireless network. Replace both of them with your credentials to ensure a successful connection.

const char* ssid = "PTCL-BB"; //replace with your SSID
const char* password = "********"; //replace with your password

GPIO Output Symbolic Names

Then, we will define the variable Led_Pin to save the GPIO pin through which the on-board LED will be connected i.e., GPIO2. The ‘GPIO_State’ variable will be used to store the current LED state. This will be used later on in the program code. We will initialize it with 0 because we start with an off LED.

bool GPIO_State = 0;
const int Led_Pin = 2;

Creating AsyncWebServer and Socket Objects

The AsyncWebServer object will be used to set up the ESP32 web server. We will pass the default HTTP port which is 80, as the input to the constructor. This will be the port where the server will listen to the requests. We will also create an object for the AsyncWebSocket called ‘ws’. This will be used on the /ws path to handle connections appropriately.

AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

Creating Web Page

We will create an html_page variable to store all the HTML, CSS, and JavaScript text which will be required to build our web page. It is more convenient to have separate HTML, CSS, and JavaScript files saved on the ESP32 SPIFFS. All of these three files will later be linked together. For this article, we will not be using this approach but include everything inside our Arduino sketch instead.

We will start with the title of the web page. The <title> tag will indicate the beginning of the title and the </tile> tag will indicate the ending. In between these tags, we will specify “ESP32 WEB SERVER” which will be displayed in the browser’s title bar. You can use any title according to your preference.

<title>ESP32 WEB SERVER</title>

Next, we will create a meta tag to make sure our web server is available for all browsers e.g., smartphones, laptops, computers etc.

<meta name="viewport" content="width=device-width, initial-scale=1">

CSS styling

Inside the html_page variable we have the <style> </style> tags which mark the beginning and end of the CSS styling file.

 We will set the display text to font type Times New Roman and align it in the centre of the webpage. For all the different texts/button, the font size, font type, colour, positioning and everything relating to the overall visuals of the web page will be is specified. This section of code shows the CSS styling which we will incorporate in our web page.


  <style>
  html {
    font-family: New Times Roman;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #612b78;
  }
  .topnav {
    overflow: hidden;
    background-color: #612b78;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
   
  }
 
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color:#fa0f0f;  
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   
  .button:active {
     background-color:#fa0f0f ; 
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#120707;
     font-weight: bold;
   }
  </style>


HTML Body

The next step will be to define the HTML web page body. This will go inside the <body></body> tags which mark the beginning and the ending of the script. This part will include the heading of the web page and the buttons. We will include the heading of our webpage inside the <h1></h1> tags and it will be the same as that of the web browser title i.e., ESP32 WebSocket Server.

<h1>ESP32 WEB SERVER</h1>

Then, we will include the second heading in <h2> </h2> tags as ‘ONBOARD LED GPIO2’.

<h2>ONBOARD LED GPIO2</h2>

Next, we will include two paragraphs to display texts. One for the button and another for the current GPIO state. The button will consist of the text: ‘Click to Toggle.’ We will use a placeholder for the GPIO state and place it inside %% signs like %STATE%. This will act as a variable and its value will always get replaced by the current GPIO state. This state will change dynamically whenever the toggle button is clicked . The WebSocket protocol will handle this smoothly. Also, JavaScript will be in charge of handling the received data so that the state will be updated correctly. This will be accomplished by using an id (<span id= “state”>) which we will reference in our script. The toggle button will also have an id (id= “button”) associated with it. This will help in handling the text using JavaScript.

<p><button id="button" class="button">Click to Toggle</button></p>
<p class="state">State: <span id="state">%STATE%</span></p>

JavaScript Handling WebSockets

Inside the <script></script> tags, we will include the JavaScript to handle the WebSocket. Data exchange as well as initiating the two-way connection between the client-server will be accomplished through this script when the webpage will be formed.

Firstly, we will create a gateway that will act as the entrance to the WebSocket interface. We will specify the webserver IP address (window.location.hostname) for the gateway.

var gateway = `ws://${window.location.hostname}/ws`;

Then we will create a new variable of type ‘var’ named ‘websocket’. Through the window.addwindowListener()function we will call the onload function. This will occur when the webserver will be loaded.

var websocket;
window.addEventListener('load', onload);

Next, we will define the onload() function. This takes in the parameter ‘event’ and calls the initWebSocket() function and the initButton() function. The initWebsocket() function will be used to initialize the WebSocket. Likewise the initButton() function will add the event listeners to the button.

function onload(event) {
  initWebSocket();
  initButton();
}

Initializing WebSocket

For the initWebSocket() function, we will use it to initialize the WebSocket connection. Three different callback functions will also be included to cater when a message will be received or the connection is opened/closed. Next, we will define each callback function separately.

function initWebSocket() {
  console.log('Trying to open a WebSocket connection…');
  websocket = new WebSocket(gateway);
  websocket.onopen    = onOpen;
  websocket.onclose   = onClose;
  websocket.onmessage = onMessage;
}

Now, we will define the onOpen() function in which ‘event’ is passed as an argument . When the WebSocket connection is opened, the console.log() function is called. This will write the message which is passed as an argument into the browser console. In our case, the message is ‘Connection opened.’

function onOpen(event) {
  console.log('Connection opened');
  
}

For the onClose() function, event will be passed as argument. When the WebSocket connection is closed, console.log() function is called which will log ‘Connection closed’ in the browser log. Additionally, the initWebSocket() function will be called after every 2 seconds to initiate the WebSocket connection.

function onClose(event) {
  console.log('Connection closed');
  setTimeout(initWebSocket, 2000);
} 

The onMessage() function is called whenever a new message will be received. The message will either a 1 or 0 depending on the state of the GPIO2. Thus, we will use an if-else statement to check whether the message is 1 or 0. If it is 1 then the state will be updated with the string “ON” else it will be updated with the string “OFF”. This will be saved on the id string which we initiated with the <span> tag previously.

function onMessage(event) {
  var state;
  if (event.data == "1"){
    state = "ON";
  }
  else{
    state = "OFF";
  }
  document.getElementById('state').innerHTML = state;
}

Initializing Button

Inside the initButton() function, we will obtain the button through its id which we defined earlier. An event listener will also be added to the button. We will add the ‘click’ event listener. This is because our web page works on the principle of clicking the toggle button.

function initButton() {
  document.getElementById('button').addEventListener('click', toggle);
}

Next, inside the toggle() function, we will call the websocket.send() function. As an argument, we will pass ‘toggle’ inside it. This will state that whenever the button is clicked the toggle function will be called. This function will send the ‘toggle’ message through the WebSocket.

function toggle(){
  websocket.send('toggle');
}

Handling WebSocket on the Server side (ESP32)

This section will deal with how our ESP32 board will handle the Websocket requests.

We will start by defining the notifyClients() function. Through this function, we want to notify all the connected clients of the updated state of the onboard LED when subjected to a change. We will achieve this through the textAll() method which will send the same string to all the clients on board.

void notifyClients() {
  ws.textAll(String(ledState));
}

Handling WebSocket Message

Next, we will define the handleWebSocketMessage(). This will act as a callback function. When new information is received by our ESP32 through the WebSocket, this callback function will run. Likewise, if the ‘toggle’ message is received by our ESP32 then notifyClients() will be called.

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    if (strcmp((char*)data, "toggle") == 0) {
      ledState = !ledState;
      notifyClients();
    }
  }
}
}

The following section of code will define the onEvent() function. This will enable an event listener which will handle the Websocket connection properly. There are five different values that the event (type) can take up. These are shown below:

  1. WS-EVT-CONNECT: when a client will log in
  2. WS_EVT_DISCONNECT: when the client will log out
  3. WS_EVT_DATA: when data will be received from the client
  4. WS_EVT_PONG: when a ping request will come up
  5. WS_EVT_ERROR: when an error will be received from the client
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
 void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

Initializing WebSocket (for ESP32)

This function will initialize our WebSocket protocol for the server side.

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

Inside our processor() function, we will replace the placeholder (%STATE%) with either ON or OFF depending on the value of the GPIO_State. If the GPIO_State is 1 then the placeholder will be replaced by “ON”. Otherwise, it will be replaced by “OFF”.

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if (ledState){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
}

Setup() function

Inside the setup() function, we will open a serial connection at a baud rate of 115200. By using the pinMode() function, the Led_Pin (GPIO2) will be passed as a parameter inside the function which will be configured as an output pin. We will initialize this pin to a LOW state so that initially the onboard LED will be off.

pinMode(Led_Pin , OUTPUT);
digitalWrite(Led_Pin , LOW);

We will connect our ESP32 board with the local network whose network credentials we already specified above through WiFi.begin(). After the connection will be established, the IP address of the ESP32 board will get printed on the serial monitor. This will help us in making a request to the server.

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Connecting to WiFi..");
}


Serial.println(WiFi.localIP());

Also, we will call the initWebSocket() to initialize the WebSocket communication protocol.

initWebSocket();

Handling Requests

We will configure the root / URL where our server will listen to HTTP GET requests.

The handling function will respond to the client by using the send_P() method on the request object. This method will take in four parameters. The first is 200 which is the HTTP status code for ‘ok’. The second is “text/html” which will correspond to the content type of the response. The third input is the text saved on the html_page variable. Finally, the last parameter is the processor function which will be sent as the response.

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

To start the server, we will call the begin() on our server object.

server.begin();

Infinite loop()

Inside our infinite loop() function we will control the onboard LED of our ESP32 through the digitalWrite() function. This function takes in the Led_Pin (GPIO2) and the GPIO_State as its input parameters. Notice that we are using the cleanClients() method inside our loop(). When the maximum number of clients is exceeded this function closes the oldest client. As a result, it helps in limiting the number of clients so the server does not exhaust.

void loop() {
  ws.cleanupClients();
  digitalWrite(ledPin, ledState);
}

Demonstration

Make sure you choose the correct board and COM port before uploading your code to the board. Go to Tools > Board and select ESP32 Dev Module.

select esp32 board

Next, go to Tools > Port and select the appropriate port through which your board is connected.

Click on the upload button to upload the code into the ESP32 development board.

After you have uploaded your code to the ESP32 development board press its ENABLE button.

ESP32 enable reset button

In your Arduino IDE, open up the serial monitor and you will be able to see the IP address of your ESP module.

ESP32 WebSocket Server IP address
Serial Monitor Demonstration

Copy that address into a web browser and press enter. The web server will look something like this:

ESP32 WebSocket Server using Arduino IDE
ESP32 WebSocket Web Page

Now click the toggle button. The onboard LED of the ESP32 board will light up. At the same moment, on ther clientswebpage, the state will also be updated to ‘ON’. Now keep on clicking the button and the state will automatically update accordingly and the LED will turn on/off. You can also try using multiple devices to open the web page. Whenever the toggle button will be clicked, the LED GPIO state will immediately update in all the clients.

ESP32 WebSocket Server demo with multiple clients

ESP32 WebSocket Server with SPIFFS

In the last section, we have included HTML, Javascript, and CSS files inside our Arduino sketch. We save these HTML documents inside Arduino Sketch by converting them into strings. Whenever a web client makes a request, the WebSocket server sends this string as a response which is basically a web page.

On the contrary, with SPIFFS, we can save HTML, CSS, and Javascript files in the ESP32 flash file system. Whenever a web client makes an HTTP request, we can serve these files directly from SPIFFS.

Recommended reading: ESP32 Web Server with SPIFFS (SPI Flash File System)

SPIFFS Files Organization

To build a WebSocket ESP32 web server using SPIFFS, we will create four different files: HTML, CSS, Javascript, and Arduino sketch and organize them in a project folder like shown below:

ESP32 websocket spiffs server files organization

HTML File

Copy the following content to page.html file:

<!DOCTYPE HTML><html>
<head>
  <title>ESP32 Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <link rel="stylesheet" type="text/css" href="style.css">

<title>ESP32 Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
  <div class="topnav">
    <h1>ESP32 WebSocket Server</h1>
  </div>
  <div class="content">
      <h2>ONBOARD LED GPIO2</h2>
       <p><button id="button" class="button">Click to Toggle</button></p>
       <p class="state">State: <span id="state">%STATE%</span></p>

  </div>
  </div>
  <script src="script.js"></script>
</body>
</html>

CSS File

Copy the following content to style.css file.

 html {
    font-family: New Times Roman;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #612b78;
  }
  .topnav {
    overflow: hidden;
    background-color: #612b78;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto; 
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color:#fa0f0f;  //green
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
  .button:active {
     background-color:#fa0f0f ; 
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#120707;
     font-weight: bold;
   }

JavaScript File

Copy the following content to script.js file.

  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  window.addEventListener('load', onLoad);
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');

  }
  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }
  function onLoad(event) {
    initWebSocket();
    initButton();
  }
  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }
  function toggle(){
    websocket.send('toggle');
  }

Arduino Sketch

Finally, copy the following code to Arduino and name it as ESP32_WebSocket_SPIFFS_Server.ino.

// Import required libraries
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"

// Replace network credentials with your own WiFi details
const char* ssid = "Enter-Wifi_Name";
const char* password = "Enter-password";

bool GPIO_State = 0;
const int Led_Pin = 2;
String stateled = "OFF";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");


void notifyClients() {
  ws.textAll(String(GPIO_State));
}

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    if (strcmp((char*)data, "toggle") == 0) {
      GPIO_State = !GPIO_State;
      notifyClients();
    }
  }
}

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
             void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if (GPIO_State){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
}
void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
  if (!SPIFFS.begin()) {
    Serial.println("An error has occurred while mounting SPIFFS");
  }
  else{
   Serial.println("SPIFFS mounted successfully");
  }
  pinMode(Led_Pin, OUTPUT);
  digitalWrite(Led_Pin, LOW);
  
  // 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());

  initWebSocket();

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/Page.html", "text/html",false,processor);
  });
   server.serveStatic("/", SPIFFS, "/");

  // Start server
  server.begin();
}

void loop() {
  ws.cleanupClients();
  digitalWrite(Led_Pin, GPIO_State);
}

Demonstration

After you have saved all three files such as Page.html, script.js, and style.css in the data folder ‘data’, upload the sketch to Arduino.

Make sure you choose the correct board and COM port before uploading your code to the board. Go to Tools > Board and select ESP32 Dev Module. Next, go to Tools > Port and select the appropriate port through which your board is connected.

select esp32 board

Click on the upload button to upload the code into the ESP32 development board.

Now, we will upload the files to our ESP32 board. Go to Tools > ESP32 Data Sketch Upload. After a few moments, the files will be uploaded.

If you don’t find “ESP32 Data Sketch Upload” option in your Arduino IDE that means you have not installed the file system uploaded plugin in Arduino IDE. You can read the following article to install a plugin:

Follow this link: Install ESP32 Filesystem Uploader in Arduino IDE

SPIFFS sketch data upload

After you have uploaded your code and the files to the ESP32 development board, press its ENABLE button.

ESP32 enable reset button

In your Arduino IDE, open up the serial monitor and you will be able to see the IP address of your ESP module.

ESP32 WebSocket Server IP address

Copy this ESP32 IP address into a web browser and press enter. The web server will look something like this:

ESP32 WebSocket Server using Arduino IDE

Conclusion

In summary, in this tutorial, we have learned how to create an ESP32 web server using WebSocket communication protocols, which support full-duplex communication between the user and the client. One of the main advantages of using WebSocket to create ESP32 and ESP8266 web servers is that the server can notify all clients whenever something changes on the server or any client-side, such as LED state, sensor output, etc.

Secondly, we have also created an ESP32 WebSocket server with a SPIFFS file system. We learned how to store CSS, JavaScript, and HTML files in the SPI flash file system of ESP32 and build a WebSocket web server through those files. Instead of hard coding the HTML as string literals which takes up a lot of memory, the SPIFFS will help us access the flash memory of the ESP32 core.

For demonstration purposes, we have controlled the onboard LED of ESP32. But you can also control a relay. You can check this project to see How to control relay with ESP32 but in the following project, we used HTTP protocol. But you can modify use the above code and modify it according to your requirement.

If you like this ESP32 web server project, you can read more here:

11 thoughts on “ESP32 WebSocket Server using Arduino IDE – Control GPIOs and Relays”

  1. HELLO
    YOUR ARTICLE IS EXCELLENT THANK YOU FOR SHARING IT I WANT TO KNOW IF MORE BUTTONS CAN BE ADDED TO EACH OF THE GPIO AND THAT SEVERAL DEVICES CAN BE CONTROLLED FROM THE SAME PAGE AT THE SAME TIME.

    Reply
  2. Como podría hacer para añadir un boton fisico
    Ya intenté hasta donde pude pero no me salió bien
    Quisiera este ejemplo con un boton fisico
    Para poder analizarlo
    Gracias

    Reply
    • Hi,

      I don’t know Spanish but I used google translate to answer you. What I understood from your question is that you want to control an LED for both web server and push button simultaneously Right?

      Reply
      • if that was my question
        how could control control the output
        with physical button

        by the way thanks for answering me

        Reply
        • We are going to post a project on this where you can control an OLED from both push-button and a web server. If the user turns on or off LED with a push button, the state of the LED will be automatically updated on the server side also.

          Reply
  3. Is there any way to do this using a website hosted on the internet? That is, can I create a separate webpage hosted elsewhere and use it to access the esp32. Thanks for any help

    Reply
    • Yes, you can do it but in that case you have to communicate with ESP32 to control GPIOs by using HTTP Get and POST requests.

      Reply
  4. Thank you for this brilliant tutorial. I initially encountered a problem in which both CSS and js files were not served to the webpage. below is the solution I discovered.

    I noticed SPIFFS is case-sensitive. If the stylesheet and javascript file name starts with a Capital letter, their corresponding reference in the HTML file must use the same capital letter. and vice versa.

    if in the Data folder you have Style.css and Script.js

    then in HTML, it should be

    for “stylesheet” href=”Style.css” and for script src=”Script.js”

    To avoid this, use small letters all through.

    Reply

Leave a Comment