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

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

Seguimos con las entradas del ESP8266 y el ESP32 viendo los ampliamente oídos, amados, y a veces poco entendidos Websockets.

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.

Las últimas entradas de esta serie de tutoriales dedicados al ESP8266 hemos visto cómo comunicar una página web con el ESP8266 como backend. Empezamos viendo los formularios web como una forma sencilla. Pero ya hemos dicho que son un mecanismo anticuado y que obliga al cliente a refrescar la página web al enviar información.

En la entrada anterior vimos una forma mucho más conveniente con peticiones Ajax. Las peticiones Ajax permiten tramitar una solicitud al servidor sin recargar la página. Pero tienen la desventaja de que el servidor no tienen un mecanismo para llamar al cliente y, además, son relativamente lentos porque tienen que generar una conexión en cada petición.

En esta entrada vamos a ver los Websockets, otro mecanismo de comunicación entre el cliente y el servidor, que permite una comunicación bidireccional y con bajo lag.

Los Websockets son una “versión moderna” de los sockets tradicionales que funcionan sobre HTTP, y están diseñados para funcionar en aplicaciones web. En un websocket se crea una conexión entre el cliente y el servidor que se mantiene abierta. Por este motivo tienen un lag mucho menos, y la comunicación puede realizarse en ambas direcciones.

Sin embargo, hay que descartar que los Websockets no son la solución definitiva o que sustituya a Ajax, sino que son tecnologías compatibles. Incluso es posible aplicaciones híbridas, que emplean ambas soluciones simultáneamente.

Los Websockets son adecuados para aplicaciones que requieran una alta velocidad de refresco (captura de datos en tiempo real, control de sistemas de luces, controlar un motor, etc) y donde sea necesaria una comunicación rápida desde el servidor al cliente.

Sin embargo, mantener la conexión abierta supone un consumo de recursos, lo que se traduce en una menor cantidad de clientes que podemos atender frente a una solución basada en un API servido a través de Ajax.

Ejemplos de código

Ya vale de introducción. Nos metemos en un ejemplo de Websockets en el ESP8266. Vamos a hacer el mismo ejemplo que hicimos en la entrada sobre Ajax, simplemente actualizando un número en la página web con el valor de ‘millis()’ obtenido desde el servidor. Un ejemplo mínimo que permite ilustrar la comunicación sin despistar con elementos adicionales.

De hecho, vamos a ver dos ejemplos sobre el mismo código (el primero de ellos está comentado). En ambos casos el cliente va a realizar una conexión de websocket, y la diferencia es:

  • 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.

Vamos a ver cómo queda nuestro código.

En nuestro programa principal, hemos añadido las dependencias adicionales a la librería ‘WebSocketsServer.h’ y referencias a ‘WebSockets.hpp’ referencia a ‘ESP8266_Utils_WS.hpp’ que va a contener código repetitivo relativo al uso de Websockets.

Por otro lado, vemos que en el loop llamamos a ‘websocket.loop()’, que gestiona el proceso de Websockets. Y también tenemos parte del código del ejemplo 2, donde el servidor usa la función ‘broadcastTXT(…);’ para informar a todos los clientes del valor de ‘millis()‘.

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

#include "config.h"  // Sustituir con datos de vuestra red
#include "WebSockets.hpp"
#include "Server.hpp"
#include "ESP8266_Utils.hpp"
#include "ESP8266_Utils_WS.hpp"

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

void loop(void)
{
  webSocket.loop();
  
  // Ejemplo 2, llamada desde servidor
  String message = GetMillis();
  webSocket.broadcastTXT(message);
}

Nuestro fichero ‘server.hpp’ queda muy simplificado. No tenemos endpoints, y simplemente servimos el contenido desde el SPIFFS.

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 al fichero ‘ESP8266_Utils_WS.hpp’, contiene código que podemos reutilizar para su uso con Websockets.

En este fichero, en primer lugar definimos un nuevo Websocket asociado al puerto 81. Al final, tenemos la función ‘InitWebSockets()’, que inicia el Websocket y asocia la función de callback ‘webSocketEvent(…)’ a los eventos de Websockets.

En la función de callback, discriminamos el tipo de evento recibido. En caso de que sea un texto recibido, generamos una respuesta con ‘ProcessRequest()’ y la enviamos al cliente.

WebSocketsServer webSocket = WebSocketsServer(81);

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght) 
{
  switch(type) {
  case WStype_DISCONNECTED:
    break;
  case WStype_CONNECTED:
    //IPAddress ip = webSocket.remoteIP(num);
    //webSocket.sendTXT(num, "Connected");
    break;
  case WStype_TEXT:
    String response = ProcessRequest();
    webSocket.sendTXT(num, response);
    break;
  }
}

void InitWebSockets() {
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
  Serial.println("WebSocket server started");
}

Finalmente, tenemos el archivo ‘WebSockets.hpp’, que contiene la definición de nuestro ‘API’ websocket. Aquí definimos la función ‘ProcessRequest()’, que hemos empleado en el fichero anterior. En este ejemplo, simplemente devolvemos el valor de ‘millis()’ codificado como string.

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

String ProcessRequest()
{
  return GetMillis();
}

Respecto al frontend tenemos

El fichero ‘index.html’ es exactamente igual al que vimos en el ejemplo Ajax.

<!DOCTYPE html>
<html class="no-js" lang="">
   <head>
      <meta charset="utf-8">
      <meta http-equiv="x-ua-compatible" content="ie=edge">
      <title>ESP8266 WebSockets</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 cambia es el fichero ‘main.js’. En este código en Javascript, vemos que tenemos la función ‘updateCounterUI(…)’ que ya usamos en el ejemplo anterior, y que simplemente actualiza el div counter con el valor oportuno.

Por otro lado, creamos un Websocket a la dirección del servidor en el puerto 81. A continuación asociamos los distintos eventos a sus funciones de callback. En particular, el evento ‘onmessage()’ lanza la función ‘updateCounterUI(…)’ con el valor recibido.

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

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

var connection = new WebSocket('ws://' + location.hostname + ':81/', ['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');
};

En el ejemplo 2, en el que el servidor hace un broadcast a todos los clientes, no es necesario más código que este.

En el ejemplo 1, en el que el cliente realiza periódicamente peticiones al servidor, tenemos el código comentado en el evento ‘onopen’. Aquí, definimos un temporizador cada 100ms, para enviar un texto (en este caso, un texto vacío), al servidor y “provocar” que nos envíe la respuesta.

Resultado

Subimos todo al ESP8266 y accedemos a la página web, para ver que, efectivamente, el valor de ‘millis()’ se actualiza correctamente. Igual que en el caso de Ajax pero muchísimo más rápido.

esp8266-ajax-resultado

¡Ya hemos terminado! Ya hemos visto cómo hacer formularios web, cómo usar peticiones Ajax y cómo usar Websockets, como formas de comunicar el frontend con el ESP8266 como backend.

En la próxima entrada veremos cómo hacer Websockets asíncronos, y en la siguiente conexiones UDP. ¡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