Envío y recepción de datagramas o paquetes UDP con ESP32


Siguiendo con esta saga de notas sobre la transferencia de datos a través de internet utilizamos ahora el protocolo de datos de usuario o UDP ampliamente difundido por su simpleza de operación ya que no depende del inicio de sesión en servidores como sucede con el HTTP que ya vimos en otra nota la cual está disponible haciendo clic acá o bien por TCP de lo cual aún no me he ocupado de hacer nota alguna pero ya llegará!. La transferencia de datos a través del protocolo UDP es mas bien comparable a una emisión de radio comercial que a una comunicación de computadoras puesto que quien envía el paquete o datagrama como se lo suele llamar lo hace a ciegas a una dirección IP y a determinado puerto de comunicaciones y si llega bien y si no, pues también bien! Por supuesto que se pueden elaborar por encima de la estandarización del UDP mecanismos de verificación de recepción y validez de los datos como tiene el TCP pero eso ya depende de cada quien y que pretenda lograr, el estándar a rajatabla nos da lo que veremos hoy en esta publicación y es mas que suficiente para muchas aplicaciones. Así como es de simple en su desarrollo es fácil de operar puesto que prácticamente no insume ni recursos del hard donde se implemente ni capacidad de infraestructura puesto que son unos míseros bytes perdidos por ahí en un mar de datos.

Nos encontramos con los dos pulsadores uno rojo y otro negro del otro circuito conectados en los pines 23 y 19 del ESP32. ya expliqué en esa nota porqué al botón colorado lo llamo Yojo! así que no vale la pena hacerlo nuevamente.  Pueden dercargar el sketch haciendo clic acá. Vemos que el sketch inicia incluyendo las librerías WiFi.h para hacer uso de dicha tecnología y WiFiUdp.h para poder enviar y recibir paquetes UDP a través del WiFi. Luego declaramos los únicos dos pulsadores del circuito, ni el LED se usa en esta oportunidad bien rasposo el circuito! Las siguientes dos declaraciones de constantes son para  los datos de conexión WiFi donde deberán poner los datos de la red de donde estén haciendo las prácticas. Aquí preferí no usar la librería miWIFI.h que hice para setear el WIFI por terminal puesto que no es la idea hacer un programa complicado sino lo mas simple posible. El entero sin signo (o sea de sólo valores positivos) puertoLocal se encarga de contener el número de puerto de comunicaciones que vayamos a destinar para la comunicación UDP. En la internet tradicional el 80 y el 8080 estan dedicados a las transferencias de hipertexto o páginas web, los puertos 110 y 25 a los mails tradicionales aunque algunos proveedores usan 587, 143 y hasta el 669 creo, el 21 para la transferencia de archivos o ftp y el 5000 para los teléfonos IP. Cada servicio tiene su puerto y con correctas normas de enrutamiento y mantenimiento de constancias mínimas se logra una calidad de servicio o QoS apropiados. Pero eso excede por lejos los alcances de esta publicación, solo se ha mencionado para que tengamos presente porqué ese número. Son satánicos y quieren ponerle 666 ? Seguro va a funcionar bien! Están muy encerrados en esta cuarentena, extrañan a su pareja y quieren ponerle 69! Seguro funcione también! Mas en entornos cerrados, el tema es cuando uno hace que estas comunicaciones trasciendan el ámbito local ahí es donde se empiezan a producir errores por colisión de datos. Siguiendo con nuestro sketch tenemos un arreglo de caracteres o char array formado de 255 de largo llamado bufferRX precisamente se va a encargar de hacer las veces de memoria entrante donde se irán guardando los datos recibidos por UDP. Declaramos una instancia del objeto WiFiUDP al cual llamamos precisamente instanciaUDP en honor a su función. En la función de configuración establecemos el sentido de operación de los pines destinados a los pulsadores activando sendas resistencias de puesta en alto en el interior del microcontrolador para evitarnos colocarlas por fuera, damos inicio a la comunicación serie, conectamos a la red WIFI e iniciamos la comunicación UDP quedando ya habilitada la recepción de datos en ese mismo momento.

El programa en sí, representado por la función loop() no hace mas que ir a la función que revisa si tiene datos por recibir vía UDP y mirar si se ha pulsado alguno de los botones. Eso es todo lo que hace y tengan siempre presente que un programa debe ser en lo posible siempre así con un loop() preciso, compacto, conciso y entendible. Para cada actividad existirá luego una función específica. Esa es la filosofía del C.

La función datosEntrantes() se encarga de verificar si se han recibido datagramas o datos por UDP y estos deben ser mostrados por la terminal serie. Dentro del objeto WiFiUDP la función parsePacket() nos devuelve el tamaño del paquete de datos o datagrama que hemos recibido vía UDP. Puede, o no, parecernos necesario conocer dicho dato. A mi para este ejemplo extremadamente minimalista me alcanzó con saber si tengo (mas de cero) o no tengo (cero) datos recibidos. Si es cero no hace nada de lo que sigue y por ende retorna al loop() sí como llego. En cambio si hay datos se ejecuta lo contenido dentro del if que básicamente es mostrar la IP de quien nos envía el datagrama, el puerto que usó en su equipo (es importante conocer esto ya veremos porqué) y el mensaje que nos envió o información en sí que es recibida en el bufferRX por medio de la función read del objeto WiFiUDP por suerte de una vez los 255 caracteres que como máximo puede contener el char array destinado para ello. Luego se trunca la cadena poniendo un 0 (el famoso \0 visto mas de una vez en los sketch) evitando que todo lo que quede posterior a él salga por puerto serie al terminal. Y voy a dedicar un poco mas de tiempo a este truco para aquellos que no lo tengan muy visto, quienes ya lo hayan pescado se pueden saltar el siguiente párrafo si quieren.

En otros programas donde use char arrays para almacenar datos que van llegando cuando ya he procesado o hecho el trabajo que tenía que hacer en dicho dato limpio todas sus posiciones poniendo un espacio en cada casillero. En el programa que estamos viendo hoy sería hacer esto:

for(int ciclo=0;ciclo<255;ciclo++) bufferRX[ciclo]=' ';

Esto recorre todo el char array poniendo un espacio en cada una de sus posiciones quedando como una cadena llena de espacios o visiblemente vacía. Porqué se hace esto ? Porque de no hacerlo si en un primer envío, estando el buffer sin texto alguno recibo la palabra HOLA por UDP veré en la terminal serie HOLA. Si luego me mandan por UDP la palabra PABLITO yo recibiré en la terminal la palabra PABLITO y si luego me mandan por UDP la palabra COMO en la terminal serie veré COMOITO  ¿? y si luego me mandan por UDP la palabra ESTAS a mi por terminal me llegará ESTASTO ¿?. Sucede que los nuevos bytes recibidos se irán guardando a partir de la posición 0 del char array como es de esperarse y como tiene que ser pero de no ocupar al menos el mismo largo que el datagrama recibido anteriormente quedará parte del anterior sin pisarse! El for mostrado arriba resuelve ese problema porque antes de cada recepción se asegura tener el array limpio pero el truco del \0 es igual de eficiente porque la función println del objeto Serial se encarga precisamente de enviar por terminal hasta en tanto no encuentre el fin de línea o el caracter nulo que curiosamente es el 0 o mejor llamado \0. Al indicarle al código que ni bien termina de recibir justo después del último caracter plante un \0 lo que sucede es lo siguiente: Recibimos primeramente un datagrama diciendo HOLA PABLITO COMO TE VA ? y el programa lo termina con \0 luego del signo de pregunta. El char array queda armado con HOLA PABLITO COMO TE VA ?\0 y por terminal serie yo recibo HOLA PABLITO COMO TE VA ?. Seguidamente me mandan por UDP el datagrama TENES FRIO ? a lo que el programa termina con \0 y el char array queda armado TENES FRIO ?\0COMO TE VA ?\0 Cuando ejecuto el Serial.println me envía hasta encontrar el primer \0 o sea que por terminal recibo TENES FRIO ? sin el primer \0, sin el resto de la cadena residual ni el segundo \0. Ese es el motivo por el cual se planta un \0 ahí. Y es muy válido aunque no milagroso! Tengan presente que eso para la función println funciona bien pero para un if por ejemplo si no lo miran no lo veran! Lo que no se programa, NO SUCEDE!

Siguiendo con el programa la función enviaDatagrama(int boton) se encarga precisamente enviar un paquete UDP por la red WIFI y dependerá del pulsador presionado el valor que adquiera boton y por ende el texto que conforma el datagrama enviado tendrá distinto número. Definimos un tipo de dato IPAddress al que llamamos sinIP y lo cargamos con sus cuatro octetos en 0. Luego usamos en el if este registro sinIP para compararlo con la dirección IP de quien sea el último que nos mandó algún datagrama puesto que será a quien le enviemos este aviso. Porque hago esto ? Para no mandar a 0.0.0.0. Sólo si previamente a presionar alguno de los botones recibimos algún datagrama es que sabremos a quien enviarle sería como estar a ciegas hasta que alguien nos da un indicio de donde está. Entonces, si la dirección IP de quien nos ha enviado algo es distinta a sinIP o sea, si no es 0.0.0.0 y adicionalmente si el puerto del cual nos enviaron no es 0 (lo cual tampoco es posible! porque al menos debe ser 1) recién ahí podemos enviar alguna trama UDP. Caso contrario no hace lo que está dentro de los corchetes del if, espera que suelte pulsadores para evitar repetir sin deseos del operador y regresa al loop(). Estando posibilitado a enviar un paquete primeramente genera el encabezado del mismo con la función beginPacket del objeto WiFiUDP función que por supuesto requiere como datos la dirección y el puerto de destino. Seguidamente escribimos el mensaje por medio de la función write del objeto WiFiUDP byte a byte (en caso de tener que hacer muchos envíos seguramente convenga armar una función que recorra un char array o bien un objeto String pero en este ejemplo con estos cinco write alcanza) y por último enviamos el dato variable que depende del valor presente en boton. Para que en el destino salga un 1 hay que enviar un 49 mientras que para enviar un 2 el valor a enviar es 50 esto porque ? presionen la tecla ALT izquierda y sin soltarla escriban en el la sección numérica ubicada a la derecha del teclado el número 50 al soltar ALT aparecerá el 2. Los datos en una tarminal son reflejados en ASCII y precisamente un terminal UDP no es mas que un terminal al fin! Ya que el ASCII del 1 es 49 y el ASCII del 2 es 50 con sumarle 48 al dato recibido por boton alcanza para convertir en ASCII el dato a enviar y lograr así que del otro lado llegue un 1 o un 2. Terminado de generar el datagrama se lo envía con la función endPacket del objeto WiFiUDP y nuestro trabajo ha terminado!.

La última función del programa conectaWIFI() se encarga precisamente de conectarse a la red WIFI de la cual hemos colocado los datos mas arriba en la declaración de constantes.

Para las experiencias con este código debemos contar con un programa apropiado para el envío y recepción de datagramas, a mi me gusta mucho el que he dispuesto para su descarga en la sección de programas para bajar de este sitio llamado PacketSender, se los recomiendo! De echo con él es que verán las capturas de pantalla de estas pruebas que hice.

Primeramente vamos a cargar el sketch en el ESP32, luego iniciamos el terminal serie y veremos que nos ha asignado una dirección IP, tomamos nota de la misma. A mi me dio la 192.168.43.22 y usaré el puerto 6543 en el ESP32 y el puerto 63656 en el PacketSender. El puerto del ESP32 lo elegí porque me gustó la seguidilla descendente y el del programa en la PC porque así vino y no lo cambié. Como ven no usé ciencia alguna en ninguno de los dos lados!.

Escribo un mensaje en el casillero ASCII puesto que lo voy a escribir como quiero leerlo al recibirlo, o sea en letras legibles y coloco la IP de destino y el puerto de destino, luego presiono el botón que dice Send y listo, el paquete se envió a la red. A destino ? Bueno, ojalá que si! Pero inicialmente a la red, llegue o no llegue es otro cantar!

Pero como ven, llegar LLEGO !!!! Gracias al simple pero suficiente programa cargado en el ESP32 me sale quien me manda el mensaje, usando que puerto en notación estándar donde la IP y el puerto se ponen separados por un dos puntos y luego de la palabra dice el mensaje en si! Todo un éxito al primer intento. Luego trabajé en la función que envía mensajes desde el ESP32 y tras algunas pruebas, errores, aciertos y un poco de trabajo he logrado esto:

Ahí pueden ver la llegada de los mensajes BOTON 1 primero y BOTON 2 después!

Son muchos los usos que se le puede dar a este tipo de comunicación al igual que a los envíos por medio de GET como vimos en la otra nota. En lo personal soy amante de las cosas simples, fáciles de implementar, que no requieran grandes cantidades de infraestructuras que en general no son libres ni gratuitas como sucede con todo esto que venimos viendo. Por supuesto que cuando uno usa algo pago en general accede a mas beneficios y funciones pero la pregunta del millón siempre es: se justifica ? Si la respuesta es SI, vamos a por la billetera pero si no se justifica es ahí donde estas opciones son mas viables.

Espero que les haya gustado y les sea de utilidad, como siempre los invito a que me envíen sus materiales que quieran compartir con otros, con gusto los voy a publicar.

 

Pablo Canello, 04/05/2020