esp8266-protocolo-udp

Cómo comunicar un ESP8266 o ESP32 por protocolo UDP

Continuamos con las entradas del ESP8266 y el ESP32 viendo cómo establecer una conexión mediante protocolo UDP como una alternativa centrada en la rapidez de la comunicación.

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.

Llevamos varias entradas viendo formas de comunicar el ESP8266 con el cliente. Hemos visto los formularios web como solución sencilla (y algo obsoleta), y las más modernas conexiones Ajax, los websockets, y los websockets asíncronos.

Todas estas soluciones funcionan mediante HTTP sobre TCP. Pero, a veces, nos olvidamos de que también existen las comunicaciones UDP. De forma muy resumida, recordamos que las comunicaciones UDP (Universal Datagram Protocol) prescinden de parte de los paquetes necesarios para crear la conexión y la verificación de errores.

En una comunicación UDP el servidor envía paquetes sin esperar acuse de recibo del cliente. Si un paquete se pierde, el cliente no puede solicitar que se reenvíe. Por tanto, TCP es útil para aplicaciones que requieren confiabilidad en la comunicación. Mientras que UDP es útil para transmisiones rápidas, incluso mayor que un Websocket.

Y en el caso de un microprocesador como el ESP8266, una comunicación UDP encaja en muchos casos. Además, las comunicaciones UDP suponen una menor carga para el servidor ya que evita una buena parte de los paquetes requeridos.

Por ejemplo, si estamos enviando una animación a una serie de LEDs, o el control de posición de un robot. En estos casos de comunicación “casi continua” no me importa tanto el acuse de recibo, si no velocidad. Si un paquete se pierde, será inmediatamente sustituido por el siguiente.

Ejemplo de código

Afortunadamente, implementar una comunicación UDP en el ESP8266 es muy sencillo gracias a la librería ‘WiFiUDP.h’. Vamos a verlo con un ejemplo.

En primer lugar, el bucle principal del programa queda de la siguiente forma.

Veremos que los únicos puntos relevantes son que hemos incluido los ficheros oportunos, y las funciones ‘ConnectUDP()’ y ‘GetUDP_Packet()’ que veremos a continuación.

También tenemos comentada la función ‘SendUDP_Packet(“abcde”)’ que ilustraría el envío de un String por UDP. No la usaremos en este ejemplo, pero ahí está.

#include <ESP8266WiFi.h>
#include <WiFiUDP.h>

#include "config.h"  // Sustituir con datos de vuestra red
#include "UDP.hpp"
#include "ESP8266_Utils.hpp"
#include "ESP8266_Utils_UDP.hpp"

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

  ConnectWiFi_STA();
  ConnectUDP();
}

void loop() 
{
  GetUDP_Packet();
  
  //SendUDP_Packet("abcde");
}

Por otro lado, tenemos el fichero ‘UDP.hpp’, en el que hemos metido todas las funciones relativas al UDP de nuestro programa.

En este fichero, hemos instanciado un objeto ‘WiFiUDP’, los puertos en los que funcionará la conexión, y definido la función ‘ProcessPacket(String response)’ que recoge la respuesta que queremos dar ante una petición UDP.

// UDP variables
WiFiUDP UDP;

unsigned int localPort = 8888;
unsigned int remotePort = 8889;
char packetBuffer[UDP_TX_PACKET_MAX_SIZE]; //buffer to hold incoming packet,

void ProcessPacket(String response)
{
   Serial.println(response);
}

Finalmente, tenemos el fichero ‘ESP8266_Utils_UDP.hpp’ en el que, siguiendo la filosofía de esta serie de entradas, hemos definido una serie de funciones comunes que podemos reaprovechar entre programas.

Aquí tenemos la función ‘ConnectUDP()’ que establece la conexión UDP, las funciones ‘SendUDP_ACK()’ que envía un acuse de recibo, ‘SendUDP_Packet(String content)’ que envía un String por UDP, y la función ‘GetUDP_Packet(bool sendACK = true)’ que recibe un paquete UDP y lo procesa con la función que hemos definido en el fichero anterior.

boolean ConnectUDP() {
  Serial.println();
  Serial.println("Starting UDP");

  // in UDP error, block execution
  if (UDP.begin(localPort) != 1) 
  {
    Serial.println("Connection failed");
    while (true) { delay(1000); } 
  }

  Serial.println("UDP successful");
}

void SendUDP_ACK()
{
  UDP.beginPacket(UDP.remoteIP(), remotePort);
  UDP.write("ACK");
  UDP.endPacket();
}

void SendUDP_Packet(String content)
{
  UDP.beginPacket(UDP.remoteIP(), remotePort);
  UDP.write(content.c_str());
  UDP.endPacket();
}

void GetUDP_Packet(bool sendACK = true)
{
  int packetSize = UDP.parsePacket();
  if (packetSize)
  {
    // read the packet into packetBufffer
    UDP.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);

    Serial.println();
    Serial.print("Received packet of size ");
    Serial.print(packetSize);
    Serial.print(" from ");
    Serial.print(UDP.remoteIP());
    Serial.print(":");
    Serial.println(UDP.remotePort());
    Serial.print("Payload: ");
    Serial.write((uint8_t*)packetBuffer, (size_t)packetSize);
    Serial.println();
    ProcessPacket(String(packetBuffer));

    //// send a reply, to the IP address and port that sent us the packet we received
    if(sendACK) SendUDP_ACK();
  }
  delay(10);
}

Subimos todo a nuestro ESP8266, y vamos a probar nuestra comunicación UDP. Para ello, os disponemos dos pequeños script en Python.

El script ‘recieveUDP.py’ que recibe un paquete UDP y lo muestra por la pantalla.

import socket

UDP_PORT = 8889

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', UDP_PORT))

while True:
  data, addr = sock.recvfrom(1024)
  print("received message:")
  print(data.decode('utf-8'))  

El script fichero ‘sendUDP.py’ que envía un paquete de ejemplo por UDP.

import socket

UDP_IP = '192.168.43.237'
UDP_PORT = 8888
UDP_PAYLOAD = 'abcdef'

def yes_or_no(question):
    reply = str(input(question)).lower().strip()
    if reply[0] == 'y':
        return 0
    elif reply[0] == 'n':
        return 1
    else:
        return yes_or_no("Please Enter (y/n) ")
    
while True:
  try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(bytes(UDP_PAYLOAD, "utf-8"), (UDP_IP, UDP_PORT))    
    sock.close()
    print("UDP target IP:", UDP_IP)
    print("UDP target port:", UDP_PORT)
    print("message:", UDP_PAYLOAD)
    if(yes_or_no('Send again (y/n): ')):
      break
  except:
    pass

Resultado

Lanzamos primero y ‘recieveUDP.py’, que queda a la escucha, y a continuación ‘sendUDP.py’. Vemos en la consola que el paquete se envía correctamente.

arduino-esp8266-udp-send

En el puerto serie de Arduino podemos comprobar que, efectivamente, el ESP8266 recibe el paquete.

arduino-esp8266-udp-serial-port

Finalmente, como en la función de recibir del ESP8266 hemos indicado que queremos que mande una señal ACK, vemos que el script de Python recibe el mensaje de acuse de recibo.

arduino-esp8266-udp-ack

¡Todo funciona correctamente! Fácil, muy útil, y en muchas ocasiones olvidada, las conexiones UDP son otra forma de comunicar un cliente con el ESP8266.

Por supuesto, en lugar de un simple String con “abced”, normalmente trabajaremos con un fichero JSON que contengan la información que queramos, como vimos en esta entrada.

Precisamente, en la próxima entrada empezaremos a emplear ficheros JSON en el ESP8266 para consumir un 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