comunicar-una-pagina-web-con-asyncwebsockets-en-el-esp8266

Comunicar una página web con AsyncWebSockets en ESP32

  • 6 min

Async WebSockets permite servir comunicación en tiempo real desde un ESP32 usando la misma infraestructura del servidor asíncrono.

El ejemplo está orientado a ESP32. En muchos casos también puede adaptarse a ESP8266 cambiando librerías y algunos detalles de pines.

Los WebSockets son una alternativa a las peticiones Ajax para proyectos que requieren bajo lag o comunicación iniciada desde el servidor hacia el cliente.

Además, la librería AsyncWebServer incorpora soporte para Async WebSockets, lo que nos permite mantener la comunicación integrada con el servidor asíncrono.

La ventaja de los Async WebSockets respecto a otras implementaciones más sencillas es que podemos atender varios clientes sin necesidad de emplear una nueva dirección o puerto.

Por lo demás, los fundamentos de funcionamiento son los mismos: una conexión persistente entre cliente y servidor, capaz de enviar mensajes en ambas direcciones.

Vamos a ver su uso con el mismo ejemplo que vimos con Ajax y Websockets, es decir, actualizar un contador con el valor de ‘millis()’ recibido del servidor. Que, nuevamente, en realidad son 2 ejemplos.

  • Ejemplo 1: El cliente enviará datos periódicamente, y recibe ‘millis()’ como respuesta
  • Ejemplo 2: El servidor usa un broadcast para informar a los clientes del valor de ‘millis()’

El ejemplo 1 está comentado en el código. Tal cuál está, el código ejecuta el ejemplo 2, que emplea broadcast.

Sencillo, pero suficiente para ilustrar la conexión sin “enmascararlo” con elementos adicionales. ¡Vamos al trabajo!

Por un lado, nuestro programa principal es básicamente idéntico a los Websockets “normales” simplemente adaptando el nombre de los métodos a la librería.

Sólo destacar el uso de la función de broadcast, que usamos en el ejemplo 2 para informar a todos los clientes conectados.

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <WebSocketsServer.h>

#include "config.h"  // Sustituir con datos de vuestra red
#include "Websocket.hpp"
#include "Server.hpp"
#include "ESP32_Utils.hpp"
#include "ESP32_Utils_AWS.hpp"

void setup(void)
{
  Serial.begin(115200);
  SPIFFS.begin();
  
  ConnectWiFi_STA();

  InitWebSockets();
  InitServer();
}

void loop(void)
{
  // Ejemplo 2, llamada desde servidor
  ws.textAll(GetMillis());
}
Copied!

Por otro lado, nuestro fichero ‘Server.hpp’, es idéntico al caso de Websockets “normales” solo que estamos usando el propio puerto 80 para servir la página web y enviar el Websockets, en lugar de usar el puerto 81.

AsyncWebServer server(80);

void InitServer()
{ 
  server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");

  server.onNotFound([](AsyncWebServerRequest *request) {
    request->send(400, "text/plain", "Not found");
  });

  server.begin();
  Serial.println("HTTP server started");
}
Copied!

Respecto a nuestro fichero con funciones reusables para Websockets pasa a llamarse ‘ESP32_Utils_AWS.hpp’ y sí ha cambiado considerablemente respecto al anterior.

Afortunadamente, encapsulando esta parte del código en este fichero no tendremos que lidiar frecuentemente con él. Básicamente, recibimos los eventos de Async Websocket y, cuando recibimos un paquete entero, lanzamos la función ProcessRequest()

void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ 
  if(type == WS_EVT_CONNECT){
    //Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
    client->printf("Hello Client %u :)", client->id());
    client->ping();
  } else if(type == WS_EVT_DISCONNECT){
    //Serial.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id());
  } else if(type == WS_EVT_ERROR){
    //Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data);
  } else if(type == WS_EVT_PONG){
    //Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:"");
  } else if(type == WS_EVT_DATA){
    AwsFrameInfo * info = (AwsFrameInfo*)arg;
    String msg = "";
    if(info->final && info->index == 0 && info->len == len){
      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 ;
        }
      }

      if(info->opcode == WS_TEXT)
      ProcessRequest(client, msg);
      
    } else {
      //message is comprised of multiple frames or the frame is split into multiple packets
      if(info->opcode == WS_TEXT){
        for(size_t i=0; i < len; i++) {
          msg += (char) data[i];
        }
      } else {
        char buff[3];
        for(size_t i=0; i < len; i++) {
          sprintf(buff, "%02x ", (uint8_t) data[i]);
          msg += buff ;
        }
      }
      Serial.printf("%s\n",msg.c_str());

      if((info->index + len) == info->len){
        if(info->final){
          if(info->message_opcode == WS_TEXT)
          ProcessRequest(client, msg);
        }
      }
    }
  }
}

void InitWebSockets()
{
  ws.onEvent(onWsEvent);
  server.addHandler(&ws);
  Serial.println("WebSocket server started");
}
Copied!

Finalmente, tenemos el fichero ‘Websocket.hpp’, donde definimos la lógica de nuestro “API” para Websockets.

En este ejemplo sencillo, únicamente enviamos el valor de ‘millis()’ codificado como texto cada vez que recibimos una petición. Solo la usaremos en el Ejemplo 1 (pero no hace falta comentarla porque ni siquiera la llamamos desde la web).

AsyncWebSocket ws("/ws");

String GetMillis()
{
  return String(millis(), DEC);
}

void ProcessRequest(AsyncWebSocketClient * client, String request)
{
  String response = GetMillis();
  client->text(response);
}
Copied!

Por otro lado, respecto al front que servimos al cliente

El HTML queda exactamente igual que en el ejemplo de WebSockets sencillos.

<!DOCTYPE html>
<html class="no-js" lang="">
   <head>
      <meta charset="utf-8">
      <meta http-equiv="x-ua-compatible" content="ie=edge">
      <title>ESP8266 Websocket Async</title>
      <meta name="description" content="">
      <meta name="viewport" content="width=device-width, initial-scale=1">
   </head>
 
   <body>
    <h1>Millis</h1>
        <div id="counterDiv">---</div>
    </body>
  
    <script type="text/javascript" src="./js/main.js"></script>
</html>
Copied!

Lo que sí se modifica, pero levemente, es el JavaScript main.js: la única modificación es que lanzamos el WebSocket contra el propio puerto 80, en lugar de usar un puerto separado.

El código, tal cual está, corresponde con el ejemplo 2, en el que el servidor hace un broadcast a todos los clientes.

Mientras que el código comentado es para el ejemplo 1, en el que el cliente realiza periódicamente peticiones al servidor, y recibe el contenido de ‘millis()’ como respuesta.

var myDiv = document.getElementById('counterDiv');

function updateCounterUI(counter)
{
  myDiv.innerHTML = counter; 
}

var connection = new WebSocket('ws://' + location.hostname + '/ws', ['arduino']);

connection.onopen = function () {
  console.log('Connected: ');
  
  // Ejemplo 1, peticion desde cliente
  //(function scheduleRequest() {
  //  connection.send("");
  //  setTimeout(scheduleRequest, 100);
  //})();
};

connection.onerror = function (error) {
  console.log('WebSocket Error ', error);
};

connection.onmessage = function (e) {
  updateCounterUI(e.data);
  console.log('Server: ', e.data);
};

connection.onclose = function () {
  console.log('WebSocket connection closed');
};
Copied!

Resultado

Si ahora cargamos la página web veremos nuestro contador incrementándose correctamente y a toda velocidad.

esp8266-ajax-resultado

Con esto tenemos una base sólida para comunicar una página web con el ESP32 mediante Async WebSockets. Es una opción muy práctica para paneles de control, telemetría sencilla o interfaces que necesiten recibir cambios sin estar preguntando continuamente al servidor.

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github.

github-full

Versión para el ESP8266: https://github.com/luisllamasbinaburo/ESP8266-Examples

Versión para el ESP32: https://github.com/luisllamasbinaburo/ESP32-Examples