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

Comunicar una página web con AsyncWebsockets en el ESP8266 o ESP32

Seguimos con los tutoriales del ESP8266 y el ESP32 viendo cómo emplear Websockets asíncronos para comunicarnos desde una página web servida.

Haremos referencia al ESP8266, pero el mismo código es compatible para el ESP32, ajustando el nombre de las librerías. Al final tenéis el código tanto para el ESP8266 como para el ESP32.

En la entrada anterior vimos cómo emplear Websockets como una alternativa a las peticiones Ajax para proyectos que requieren bajo lag o comunicación del servidor al cliente.

Pues bien, igual que vimos como configurar un servidor, y después vimos cómo configurar un servidor asíncrono con la librería AsyncWebServer, la misma librería incorpora un plugin para Async Websockets.

Las ventajas de los Async Websockets respecto a los Websockets “normales” implementados en las librerías del ESP8266 es que podemos atender varios clientes sin necesidad de emplear una nueva dirección o puerto.

Por tanto, podríamos considerarlos “una versión mejorada” de la implementación de Websockets en el ESP8266. Por lo demás, los fundamentos de funcionamiento son iguales.

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 (igual que hicimos en la entrada anterior).

#include <ESP8266WiFi.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 "ESP8266_Utils.hpp"
#include "ESP8266_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());
}

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");
}

Respecto a nuestro fichero con funciones reusables para Websockets pasa a llamarse ‘ESP8266_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");
}

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);
}

Por otro lado, respecto al front que servimos al cliente

El HTML queda exactamente igual a la entrada anterior

<!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>

Lo que sí se modifica, pero levemente, es el Javascript ‘main.js’, siendo la única modificación que lanzamos el websocket contra el propio puerto 80, en lugar del 81 que hicimos en la entrada anterior.

Recordamos que 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');
};

Resultado

Si ahora cargamos la página web veremos nuestro contador incrementándose (y es la última vez, os lo prometo) correctamente y a toda velocidad.

esp8266-ajax-resultado

Y hasta aqui la entrada sobre Async Websockets en el ESP8266, con lo que hemos terminado de presentar las formas más habituales de comunicación entre frontend y backend.

En el próximo tutorial del ESP8266 haremos una pequeña pausa para presentar la comunicación mediante UDP. Y después volveremos a la comunicación cliente servidor ampliando lo que hemos visto sobre Ajax y Websockets con ficheros Json y API Rest. ¡Hasta pronto!

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