02 aug 2020

How to Create an ESP32 Web Server with WebSockets using the ESPAsyncWebServer websockets plugin

In my quest for finding the perfect latest webserver code to use in my future ESP32 projects I could not find exactly what I was looking for online.
I wanted an Async webserver using Websockets to control settings on my ESP32.
Then I found this nice tutorial from Shawn Hymel (Known from SparkFun):

https://shawnhymel.com/1882/how-to-create-a-web-server-with-websockets-using-an-esp32-in-arduino

It is exactly what I need, but I was surprised that it used the ESPAsyncWebServer library as well as the WebSocketServer library.
Why use a separate WebSocket library when the ESPAsyncWebServer has an WebSockets plugin build in?

(See: https://github.com/me-no-dev/ESPAsyncWebServer#async-websocket-plugin )

So I rewrote the code from Shawn to only use the ESPAsyncWebServer library.

Also, to make things easier, I added mDNS in the mix so the ESP can be reached by an easy to remember address like http://esp32.local

Have a look at the tutorial from Shawn and apply my edited code:

 

#include <Arduino.h> //for platformIO
#include <WiFi.h>
#include <ESPmDNS.h>
#include <SPIFFS.h>
#include <ESPAsyncWebServer.h>

// Constants
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";
const int led_pin = 2;        // LED PIN ON 'ESP32 Dev Kit v1'

// Globals
AsyncWebServer server(80);    // webserver on port 80
AsyncWebSocket ws("/ws");     // websocket server on /ws
char msg_buf[10];             // sending buffer
int led_state = 0;            // state of the led

// This handles the websocket event
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
{

  if (type == WS_EVT_CONNECT)
  { //on connect
    Serial.printf("Websocket client [%u] connection received\n", client->id());
    //client->text("Hello from ESP32 Server");
  }
  else if (type == WS_EVT_DISCONNECT)
  { // on disconnect
    //Serial.println("Client disconnected");
    Serial.printf("Websocket client [%u] disconnected.\n", client->id());
  }
  else if (type == WS_EVT_DATA)
  { // when receiving data

    AwsFrameInfo * info = (AwsFrameInfo*)arg;
    String msg = "";
    if(info->final && info->index == 0 && info->len == len){
      //the whole message is in a single frame and we got all of it's data
      Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len);

      // put message into msg string
      if(info->opcode == WS_TEXT){
        for(size_t i=0; i < info->len; i++) {
          msg += (char) data[i];
        }
      } else {
        char buff[3];
        for(size_t i=0; i < info->len; i++) {
          sprintf(buff, "%02x ", (uint8_t) data[i]);
          msg += buff ;
        }
      }
      Serial.printf("%s\n",msg.c_str()); //serial print the message

      // // send a reply to the client
      // if(info->opcode == WS_TEXT)
      //   client->text("I got your text message");
      // else
      //   client->binary("I got your binary message");
    }

    // Now that we have the message, act on it:
    if (msg == "toggleLED") // Toggle LED
    {
      led_state = led_state ? 0 : 1;
      Serial.printf("Toggling LED to %u\n", led_state);
      digitalWrite(led_pin, led_state);
    }      
    else if (msg == "getLEDState") // Report the state of the LED
    {
      sprintf(msg_buf, "%d", led_state);
      Serial.printf("Sending to [%u]: %s\n", client->id(), msg_buf);
      client->text(msg_buf);
    }
    else // Message not recognized
    {
      Serial.printf("[%u] Message not recognized", client->id());
    }
  }
}

// Callback: send homepage
void onIndexRequest(AsyncWebServerRequest *request) {
  IPAddress remote_ip = request->client()->remoteIP();
  Serial.println("[" + remote_ip.toString() +
                  "] HTTP GET request of " + request->url());
  request->send(SPIFFS, "/index.html", "text/html");
  //request->send(404, "text/plain", "Hello World"); // you can use this line instead, to test if the server is working
}

// Callback: send style sheet
void onCSSRequest(AsyncWebServerRequest *request) {
  IPAddress remote_ip = request->client()->remoteIP();
  Serial.println("[" + remote_ip.toString() +
                  "] HTTP GET request of " + request->url());
  request->send(SPIFFS, "/style.css", "text/css");
}

// Callback: send 404 if requested file does not exist
void onPageNotFound(AsyncWebServerRequest *request) {
  IPAddress remote_ip = request->client()->remoteIP();
  Serial.println("[" + remote_ip.toString() +
                  "] HTTP GET request of " + request->url());
  request->send(404, "text/plain", "Not found");
}

void setup() {
  // Init LED and turn off
  pinMode(led_pin, OUTPUT);
  digitalWrite(led_pin, LOW);

  // Init Serial
  Serial.begin(115200);

  // Make sure we can read the file system
  if( !SPIFFS.begin()){
    Serial.println("Error mounting SPIFFS");
    while(1);
  }

  // Connect to WiFi
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
 
  Serial.print("My IP adress is: ");
  Serial.println(WiFi.localIP());

  // Add MDNS service to easily find the device in your network
  if (MDNS.begin("ESP32")) { // http://esp32.local/
    Serial.println("MDNS responder started find me on http://esp32.local/");
  }

  // On HTTP request for root, provide index.html file
  server.on("/", HTTP_GET, onIndexRequest);

  // On HTTP request for style sheet, provide style.css
  server.on("/style.css", HTTP_GET, onCSSRequest);

  // Handle requests for pages that do not exist
  server.onNotFound(onPageNotFound);
 
  // add Websockets
  ws.onEvent(onWsEvent);
  server.addHandler(&ws);
 
  server.begin();
  Serial.println("Webserver started.");
}

void loop() {
  // put your main code here, to run repeatedly:
}

And the index.html, from which I only changed line 7:

 

<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>

<script language="javascript" type="text/javascript">

var url = 'ws://'+location.hostname+'/ws';
var output;
var button;
var canvas;
var context;

// This is called when the page finishes loading
function init() {

    // Assign page elements to variables
    button = document.getElementById("toggleButton");
    output = document.getElementById("output");
    canvas = document.getElementById("led");
    
    // Draw circle in canvas
    context = canvas.getContext("2d");
    context.arc(25, 25, 15, 0, Math.PI * 2, false);
    context.lineWidth = 3;
    context.strokeStyle = "black";
    context.stroke();
    context.fillStyle = "black";
    context.fill();
    
    // Connect to WebSocket server
    wsConnect(url);
}

// Call this to connect to the WebSocket server
function wsConnect(url) {
    
    // Connect to WebSocket server
    websocket = new WebSocket(url);
    
    // Assign callbacks
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
}

// Called when a WebSocket connection is established with the server
function onOpen(evt) {

    // Log connection state
    console.log("Connected");
    
    // Enable button
    button.disabled = false;
    
    // Get the current state of the LED
    doSend("getLEDState");
}

// Called when the WebSocket connection is closed
function onClose(evt) {

    // Log disconnection state
    console.log("Disconnected");
    
    // Disable button
    button.disabled = true;
    
    // Try to reconnect after a few seconds
    setTimeout(function() { wsConnect(url) }, 2000);
}

// Called when a message is received from the server
function onMessage(evt) {

    // Print out our received message
    console.log("Received: " + evt.data);
    
    // Update circle graphic with LED state
    switch(evt.data) {
        case "0":
            console.log("LED is off");
            context.fillStyle = "black";
            context.fill();
            break;
        case "1":
            console.log("LED is on");
            context.fillStyle = "red";
            context.fill();
            break;
        default:
            break;
    }
}

// Called when a WebSocket error occurs
function onError(evt) {
    console.log("ERROR: " + evt.data);
}

// Sends a message to the server (and prints it to the console)
function doSend(message) {
    console.log("Sending: " + message);
    websocket.send(message);
}

// Called whenever the HTML button is pressed
function onPress() {
    doSend("toggleLED");
    doSend("getLEDState");
}

// Call the init function as soon as the page loads
window.addEventListener("load", init, false);

</script>

<h2>LED Control</h2>

<table>
    <tr>
        <td><button id="toggleButton" onclick="onPress()" disabled>Toggle LED</button></td>
        <td><canvas id="led" width="50" height="50"></canvas></td>
    </tr>
</table>