
Seguimos con los tutoriales del ESP8266 viendo cómo emplear Websockets asíncronos para comunicarnos desde una página web servida.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include #include #include #include #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()); } |
Sólo destacar el uso de la función de broadcast, que usamos en el ejemplo 2 (igual que hicimos en la entrada anterior).
Por otro lado, nuestro fichero ‘server.hpp’ queda de la siguiente forma.
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”); } |
Que 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.
Respecto a nuestro fichero con funciones reusables para Websockets pasa a llamarse ‘ESP8266_Utils_AWS.hpp’ y sí ha cambiado considerablemente respecto al anterior, quedando,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
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] connectn”, 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: %un”, server->url(), client->id()); } else if(type == WS_EVT_ERROR){ //Serial.printf(“ws[%s][%u] error(%u): %sn”, server->url(), client->id(), *((uint16_t*)arg), (char*)data); } else if(type == WS_EVT_PONG){ //Serial.printf(“ws[%s][%u] pong[%u]: %sn”, 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(“%sn”,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”); } |
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()’
Finalmente, tenemos el fichero ‘Websocket.hpp’, donde definimos la lógica de nuestro “API” para Websockets.
AsyncWebSocket ws(“/ws”); String GetMillis() { return String(millis(), DEC); } void ProcessRequest(AsyncWebSocketClient * client, String request) { String response = GetMillis(); client->text(response); } |
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).
Por otro lado, el fichero ‘index.html’ que servimos al cliente queda exactamente igual a la entrada anterior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!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>
</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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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’); }; |
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.
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.
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.