Comunicación inalámbrica con ESP-NOW

Manejar comunicaciones por wifi suele ser algo complejo de programar y mantener la estabilidad en la señal, por eso se presenta este nuevo protocolo exclusivo de placas ESP desarrollado por la misma empresa Espressif. Este protocolo tiene ventajas como:
- Comunicación máster-esclavo, máster-máster, y red en malla con varios ESP a la vez.
- Cifrado punto a punto mediante MAC para garantizar seguridad y minimizar interferencias.
- Bajo consumo.
- Alta velocidad con muy poco delay entre paquetes.
- Configuración punto a punto full dúplex.
- Larga distancia de emisión (en el pasillo desde el Mercadona hasta la puerta de la academia).
La principal desventaja al estar continuamente enviando y recibiendo paquetes es el calentamiento del mismo procesador, que se puede paliar limitando los paquetes, la frecuencia y usando refrigeración. Los paquetes que se envían se pueden modificar en longitud (limitado a 250 bytes) y tipo de datos (byte, string, int…)
Vamos a empezar creando un máster que envíe un char y un byte simulando el envío de la dirección de un joystick y la velocidad del motor, pero antes necesitamos saber cuál es la MAC tanto del emisor como del receptor:
Código para conocer la MAC
Recuerda tener seleccionada la placa ESP32 normal, versión librería ESP a 3.0, el puerto y el monitor serial a 115200. Lo que obtendremos es la MAC en el formato hexadecimal XX:XX:XX:XX:XX:XX que se compone de letras y números en seis partes. Sabiendo ahora sí de la MAC tanto del emisor como el receptor las apuntaremos en alguna parte de las placas.
Cogeremos una placa que será el maestro y le copiamos este código:
Recuerda tener seleccionada la placa ESP32 normal, versión librería ESP a 3.0, el puerto y el monitor serial a 115200. Lo que obtendremos es la MAC en el formato hexadecimal XX:XX:XX:XX:XX:XX que se compone de letras y números en seis partes. Sabiendo ahora sí de la MAC tanto del emisor como el receptor las apuntaremos en alguna parte de las placas.
Cogeremos una placa que será el maestro y le copiamos este código:
Incluimos la librería de Wifi:
#include <WiFi.h>
Iniciamos el serial y ponemos el wifi en modo estático:
Serial.begin(115200);
WiFi.mode(WIFI_STA);
Printamos la MAC de la placa cada segundo:
String macStr = WiFi.macAddress();
Serial.println(macStr);
delay(1000);
Código del emisor
Incluimos las librerías de Wifi y Esp now:
#include <esp_now.h>
#include <WiFi.h>
Aquí modificamos con la MAC del otro ESP a recibir. Si apuntamos 24:0A:C4:1A:E4:F4 entonces el array será con un 0x delante:
uint8_t broadcastAddress[] = {0x24, 0x0A, 0xC4, 0x1A, 0xE4, 0xF4};
Esta es la estructura de datos de envío de nombre dataToSend donde tenemos un char y un byte:
typedef struct struct_message {
char myChar;
byte myByte;
} struct_message;
struct_message dataToSend;
Callback que se llama cuando el mensaje ha sido enviado que nos devuelve si ha tenido éxito o no se envió.
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print(“Mensaje enviado con estado: “);
Serial.println(status == ESP_NOW_SEND_SUCCESS ? “Éxito” : “Fallido”);
}
Iniciamos el ESP-NOW:
if (esp_now_init() != ESP_OK) {
Serial.println(“Error al inicializar ESP-NOW”);
return;
}
Registramos el callback anterior cuando se envía y añadimos el peer en memoria que será la placa receptora donde ponemos la MAC que modificamos antes, el canal 0 y si está encriptada la señal:
esp_now_register_send_cb(OnDataSent);
// Añadir el dispositivo peer (receptor)
esp_now_peer_info_t peerInfo;
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
En loop por ejemplo modificamos las variables a enviar (char y byte) pero añadiendo el nombre de la estructura de envío (dataToSend). Se pueden modificar en cualquier lado pues son como variables globales:
dataToSend.myChar = ‘a’;
dataToSend.myByte = 255;
Y por último la función en enviar a la MAC, la estructura de envío y el tamaño del paquete. Con llamar a la función se enviará lo que hayamos modificado y retornará si falló o tuvo éxito:
esp_now_send(broadcastAddress, (uint8_t *) &dataToSend, sizeof(dataToSend));
Código del receptor
Lo que cambia en el receptor son pocas cosas:
La MAC del emisor que es la que programamos antes:
uint8_t broadcastAddress[] = {0x84, 0x2A, 0xZ4, 0x1B, 0xE4, 0xE5};
La misma estructura pero con nombre incomingData:
typedef struct struct_message {
char myChar;
byte myByte;
} struct_message;
struct_message incomingData;
El callback para recibir y printar los datos:
void OnDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingDataBytes, int len) {
memcpy(&incomingData, incomingDataBytes, sizeof(incomingData));
Serial.print(“Char recibido: “);
Serial.println(incomingData.myChar);
Serial.print(“Byte recibido: “);
Serial.println(incomingData.myByte);
}
Agregamos el callback de recepción:
esp_now_register_recv_cb(OnDataRecv);
Y tan sólo abriremos el Monitor serial para ver si se recibe algo.
Si las MAC están correctas debería haber comunicación entre las placas. Con esto podemos personalizar la estructura de los paquetes y cuántos peers queremos conectar.
Podemos agregar los peers que queramos (sin petar la placa) tan solo sabiendo la MAC, además de enviar y recibir simultáneamente de una manera segura y rápida sin necesidad de routers. Si queremos mandar muchos dats diferentes se pueden añadir todas las estructuras y variables que necesitemos, sin saturar la memoria claramente.
¿Y si necesito saber qué MAC estoy recibiendo?
Se puede hacer descodificando el paquete de datos para sacar la MAC y luego discriminar cuáles van a ser las elegidas o si sólo se quiere escuchar una placa.
Tomamos la información recibida para sacar la mac:
const uint8_t *mac = recv_info->src_addr;
Lo pasamos a una cadena local y le aplicamos formato de tipo MAC:
char macStr[18];
snprintf(macStr, sizeof(macStr), “%02X:%02X:%02X:%02X:%02X:%02X”, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
//Serial.println(macStr);
Y decimos qué MAC es la que queremos elegir comparándola en memoria si el formato XX:XX:XX:XX:XX:XX es igual a la recogida anteriormente, retornando 0 si coincide:
uint8_t mac1[6] = {0xE0, 0x5A, 0x1B, 0xD3, 0x0C, 0x50}; // Placa machaca
if (memcmp(mac, mac1, 6) == 0) {
Serial.println(“MAC de la placa machaca”);
} else {
Serial.println(“Es otra placa, hermano”);
}
Identificar diferentes estructuras de datos
Sí, es posible discernir entre estructuras de diferentes tamaño y valores, tan sólo agregando una variable de identificación (por ejemplo “ID”)
Tenemos tres tipos de estructuras con ID al inicio:
typedef struct struct_message3 {
byte id = 20;
bool detect;
} struct_message3;
struct_message3 datosDetect;
…
Metemos temporalmente en una única variable del mismo tipo (byte) y le copiamos en memoria los datos recibidos:
byte IDdatos;
memcpy(&IDdatos, incomingDataBytes, sizeof(byte));
Serial.print(“ID: “);
Serial.println(IDdatos);
Finalmente analizamos qué valor tiene para pasarlo a memoria de la estructura a la que corresponde:
if (IDdatos == 20) { // datosDetect
memcpy(&datosDetect, incomingDataBytes, sizeof(datosDetect));
Serial.println(“Estructura de detección”);
} else if (IDdatos == 14) { // datosPantalla
memcpy(&datosPantalla, incomingDataBytes, sizeof(datosPantalla));
Serial.println(“Estructura de la pantalla);
} else if (IDdatos == 28) { // datosAqua
memcpy(&datosAqua, incomingDataBytes, sizeof(datosAqua));
Serial.println(“Estructura del Aqua”);
}
Con todo esto podemos formar una comunicación bidireccional, encriptada mediante MACs y que podamos enviar diferentes tipos de datos a una alta tasa y distancia.
Precauciones
- Esta comunicación utiliza mucho procesamiento y pone la CPU al rojo vivo así que es recomendable poner disipadores y/o refrigeración.
Notas
- El ESP32 se calentará más de lo normal.
- Si no se envían los datos puede ser porque las MAC están mal o alguna placa está desconectada.
- Si se quiere manejar datos grandes recordar que el límite son 250 bytes y se puede separar en más structs.
- Las variables int, long y string ocupan mucha memoria, es preferible manejar con variables más pequeñas.
- Enviar muchos datos separados como por ejemplo los botones de un gamepad es mejor utilizar la librería StringSplitter.