En esta ocasión te hablaré del protocolo ESP-Now, el cual podremos usarlo en nuestros mirocontroladores ESP32, ESP8266, y seguramente en el resto de familia ESP. ESP-Now es un protocolo de comunicación entre varios dispositivos creado por Espressif, el cual es similar al utilizado en los dispositivos de baja energía que funcionan en la banda de 2.4Ghz. Su funcionamiento requiere de emparejamiento de los dispositivos, pero una vez hecho la conexión será automática.

ESP32 soporta las siguientes características:

  • Comunicación unicast encriptada y sin encriptar
  • Se pueden mezclar clientes con encriptación y sin encriptación
  • Permite enviar hasta 250-bytes de carga útil
  • Se pueden configurar callbacks para informar a la aplicación si la transmisión fue correcta
  • Largo alcance, pudiendo superar los 200m en campo abierto.

Pero también tiene sus limitaciones, las cuales son:

  • El número de clientes con encriptación está limitado. Esta limitación es de 10 clientes para el modo Estación, 6 como mucho en modo punto de acceso o modo mixto.
  • El número total de clientes con y sin encriptación sin encriptación es del 20.
  • Sólo se pueden enviar 250 bytes como mucho.

En palabras simples, ESP-Now es un protocolo de comunicación que nos permitirá intercambiar pequeños mensajes (hasta 250 bytes), entre nuestros microcontroladores ESP. Este protocolo es muy versátil y nos permitirá realizar conexiones en una dirección o en ambas direcciones, en diferentes configuraciones.

Tipos de comunicación

Comunicación ESP-Now en una dirección

Este tipo de comunicación se compone de uno o varios dispositivos ESP que funcionarán como maestros y esclavos. La comunicación la iniciará el dispositivo o dispositivos maestros, y será recibida por el o los esclavos. Entre las diferentes configuraciones de las que disponemos para la configuración en una dirección, podemos distinguir las siguientes:

  • Un maestro y un esclavo
  • Un maestro y varios esclavos
  • Varios maestros y un esclavo

Un maestro y un esclavo

Esta configuración permite conectar un dispositivo ESP que hará de maestro, con otro que hará de esclavo. Es el tipo de comunicación más simple y por lo tanto es muy fácil de implementar, y es muy buena para enviar datos de una placa a otra como las lecturas de un sensor, el control de puertos GPIO…

Un maestro y varios esclavos

Esta configuración es similar a la configuración anterior, salvo que en este caso varios microcontroladores harán de esclavos. El maestro enviará los datos al mismo tiempo a todos los esclavos, por lo que es muy útil para controlar varios dispositivos ESP al mismo tiempo. Por ejemplo, como control remoto para manejar con un microcontrolador, varios desperdigados por toda la casa…

Varios maestros y un esclavo

Esta configuración nos permitirá tener una placa que hará de esclava, la cual recibirá datos de varias placas maestras. Este tipo de configuración es muy útil para enviar datos por ejemplo de varios sensores. Imagina que tienes un invernadero, y tienes varios sensores para controlar la temperatura y humedad, o si necesitan regarse las plantas. Esta configuración te permitiría enviar todos los datos de los sensores y tenerlos centralizados en uno.

Comunicación ESP-Now en dos direcciones

Este tipo de comunicación es como el anterior, con la diferencia de que ambas placas pueden actuar como remitentes y destinatarios de los mensajes, lo cual la hace más flexible. Por ejemplo, puedes tener dos placas comunicándose entre sí.

Además, otra ventaja que tiene es que podemos añadir más placas y la comunicación seguirá siendo bidireccional entre ellas como si de una red se tratase.

Comenzando: Descubre la dirección MAC de tu placa

Para poder conectar tus placas a través de ESP-Now es necesario que conozcas la dirección MAC de cada una de ellas. Para ello puedes utilizar este sketch simple, el cual te devolverá la dirección MAC de tu WiFi a través del monitor serie de la aplicación Arduino.

/*
  Daniel Carrasco
  This and more tutorials at https://www.electrosoftcloud.com/
*/

// Simple code to retreive the WiFi MAC address
#if defined(ESP32)
  #include "WiFi.h"
#elif defined(ESP8266)
  #include "ESP8266WiFi.h"
#else
  // Non supported board
  #error This board is not supported
#endif

void setup(){
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  Serial.println(WiFi.macAddress());
}

void loop(){
}

Una vez subido a nuestra placa activaremos el monitor serie de nuestro programa, lo configuraremos a 115200 baudios y veremos en la salida la dirección MAC de nuestra placa.

Guardaremos estas direcciones para más adelante usarlas en la comunicación entre nuestras placas.

Comunicación punto a punto en una dirección con ESP-Now

Para comenzar con ESP-Now, empezaremos con el método más simple de comunicación entre dos placas (en una dirección), y crearemos un proyecto simple para enviar un mensaje de la placa maestra a la esclava.

En este ejemplo crearemos una variable del tipo struct que nos permitirá enviar varias variables agrupadas de golpe sin necesidad de enviarlas por separado, algo que será muy útil para enviar los datos de varios sensores por ejemplo. En mi caso lo usaré para enviar los datos de temperatura y humedad desde mi sensor DHT22. Por ello centraré mi ejemplo enviar estos datos, pero podrás añadir más datos sin problema siempre que no superes el tamaño límite de 250 bytes.

ESP32

En este apartado os explicaré el funcionamiento y usaré el código para hacerlo funcionar en el microcontrolador ESP32. Si tu microcontrolador es un ESP8266, tendrás que pasar a la sección correspondiente.

Placa maestra ESP32 (remitente)

Lo primero que haremos, será configurar la placa que hará de maestra, que es la placa que creará y enviará el mensaje a la otra. Para ello podemos utilizar este sketch, el cual explicaré a continuación.

/*
  Daniel Carrasco
  This and more tutorials at https://www.electrosoftcloud.com/
*/

#include <esp_now.h>
#include <WiFi.h>

// Set the SLAVE MAC Address
uint8_t slaveAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Structure to keep the temperature and humidity data from a DHT sensor
typedef struct temp_humidity {
  float temperature;
  float humidity;
};

// Create a struct_message called myData
temp_humidity dhtData;

// Callback to have a track of sent messages
void OnSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("\r\nSend message status:\t");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Sent Successfully" : "Sent Failed");
}
 
void setup() {
  // Init Serial Monitor
  Serial.begin(115200);
 
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("There was an error initializing ESP-NOW");
    return;
  }

  // We will register the callback function to respond to the event
  esp_now_register_send_cb(OnSent);
  
  // Register the slave
  esp_now_peer_info_t slaveInfo;
  memcpy(slaveInfo.peer_addr, slaveAddress, 6);
  slaveInfo.channel = 0;  
  slaveInfo.encrypt = false;
  
  // Add slave        
  if (esp_now_add_peer(&slaveInfo) != ESP_OK){
    Serial.println("There was an error registering the slave");
    return;
  }
}

void loop() {
  // Set values to send
  // To simplify the code, we will just set two floats and I'll send it 
  dhtData.temperature = 12.5;
  dhtData.humidity = 58.9;

  // Is time to send the messsage via ESP-NOW
  esp_err_t result = esp_now_send(slaveAddress, (uint8_t *) &dhtData, sizeof(dhtData));
   
  if (result == ESP_OK) {
    Serial.println("The message was sent sucessfully.");
  }
  else {
    Serial.println("There was an error sending the message.");
  }
  delay(2000);
}
Explicación del código
// Set the SLAVE MAC Address
uint8_t slaveAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

En esta línea configuraremos la dirección MAC del esclavo que recibirá el mensaje. Esta dirección es el formato hexadecimal, pero no te asustes. La dirección MAC que recibiste antes también lo es, por lo que tan sólo tendrás que poner 0x antes de cada grupo de dos cifras. Por ejemplo, la dirección MAC 80:45:32:67:F8:01 se convertiría en {0x80, 0x45, 0x32, 0x67, 0xF8, 0x01}.

// Structure to keep the temperature and humidity data from a DHT sensor
typedef struct temp_humidity {
  float temperature;
  float humidity;
};

// Create a struct_message called myData
temp_humidity dhtData;

Aquí declaramos el struct que contendrá todos los datos que queremos enviar, que recordemos, puede llegar a ocupar 250 bytes. En este ejemplo tan sólo utilizamos como 8 bytes, así que piensa que todavía tienes mucho espacio para enviar datos.

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("There was an error initializing ESP-NOW");
    return;
  }

  // We will register the callback function to respond to the event
  esp_now_register_send_cb(OnSent);
  
  // Register the slave
  esp_now_peer_info_t slaveInfo;
  memcpy(slaveInfo.peer_addr, slaveAddress, 6);
  slaveInfo.channel = 0;  
  slaveInfo.encrypt = false;

  // Add slave        
  if (esp_now_add_peer(&slaveInfo) != ESP_OK){
    Serial.println("There was an error registering the slave");
    return;
  }

En esta porción de código inicializaremos el ESP-Now, regitraremos la función que será ejecutada en cada envío, y registraremos el esclavo al que queremos enviar los datos. Fíjate en que la configuración de encriptación está desactivada, pero la podremos activar simplemente cambiando slaveInfo.encrypt a true.

  // Set values to send
  // To simplify the code, we will just set two floats and I'll send it 
  dhtData.temperature = 12.5;
  dhtData.humidity = 58.9;

  // Is time to send the messsage via ESP-NOW
  esp_err_t result = esp_now_send(slaveAddress, (uint8_t *) &dhtData, sizeof(dhtData));

Y por último simplemente rellenamos la nueva variable con los datos que tengamos, que en mi caso me los invento por simplificar el código, y la enviamos a través de la función esp_now_send.

Placa esclava ESP32 (destinataria)

Esta placa será la que reciba el mensaje y reaccione a él. En este tipo de comunicación de una dirección esta placa no podrá iniciar la comunicación, por lo que simplemente se limitará a esperar que la placa maestra la inicie y le envíe los datos. En el ejemplo siguiente os muestro cómo recibir estos datos y guardarlos en una variable para posteriormente utilizarlos. Para dicho propósito utilizaremos también una función callback que nos evite tener que estar haciendo pooling para ver si se ha recibido algo.

/*
  Daniel Carrasco
  This and more tutorials at https://www.electrosoftcloud.com/
*/

#include <esp_now.h>
#include <WiFi.h>

// Structure to keep the temperature and humidity data
// Is also required in the client to be able to save the data directly
typedef struct temp_humidity {
  float temperature;
  float humidity;
};

// Create a struct_message called myData
temp_humidity dhtData;

// callback function executed when data is received
void OnRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&dhtData, incomingData, sizeof(dhtData));
  Serial.print("Bytes received: ");
  Serial.println(len);
  Serial.print("Temperature: ");
  Serial.println(dhtData.temperature);
  Serial.print("Humidity: ");
  Serial.println(dhtData.humidity);
}

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("There was an error initializing ESP-NOW");
    return;
  }
  
  // Once the ESP-Now protocol is initialized, we will register the callback function
  // to be able to react when a package arrives in near to real time without pooling every loop.
  esp_now_register_recv_cb(OnRecv);
}

void loop() {
}

Para no alargar mucho, explicaré las partes del código que difieren al código que ya hemos visto con anterioridad.

// callback function executed when data is received
void OnRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&dhtData, incomingData, sizeof(dhtData));
  Serial.print("Bytes received: ");
  Serial.println(len);
  Serial.print("Temperature: ");
  Serial.println(dhtData.temperature);
  Serial.print("Humidity: ");
  Serial.println(dhtData.humidity);
}

En esta parte definiremos una función que servirá de callback para cuando recibamos datos. Obviamente si recibimos distintos tipos de datos, habrá que comprobar su tipo antes de guardarlo, pero como en el ejemplo sólo recibimos un tipo, simplemente lo copiaremos. Esto se hace con memcpy, que es básicamente una función para realizar la copia del dato recibido en un struct previamente creado. Una vez que hemos copiado el dato, ya podemos trabajar con él sin problemas, que en este casi simplemente lo mandamos por le puerto serial.

  // Once the ESP-Now protocol is initialized, we will register the callback function
  // to be able to react when a package arrives in near to real time without pooling every loop.
  esp_now_register_recv_cb(OnRecv);

Esta función sirve simplemente para registrar la función que será llamada cuando recibamos un mensaje (callback), así que indicaremos la función que hemos creado antes.

ESP8266

Si en tu caso tus placas son ESP8266 en lugar de ESP32, el sketch cambiará ligeramente. Al igual que con el ESP32, explicaré el código para que te sea más fácil entenderlo.

Placa maestra ESP8266 (remitente)

Ahora veremos como configurar la placa maestra en el ESP8266, lo cual como verás es muy similar y tan sólo necesitaremos unos pequeños cambios:

/*
  Daniel Carrasco
  This and more tutorials at https://www.electrosoftcloud.com/
*/

// ESP8266 Version
#include "ESP8266WiFi.h"
#include <espnow.h>

// Set the SLAVE MAC Address
uint8_t slaveAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Structure to keep the temperature and humidity data from a DHT sensor
typedef struct temp_humidity {
  float temperature;
  float humidity;
};

// Create a struct_message called myData
temp_humidity dhtData;

// Callback to have a track of sent messages
void OnSent(uint8_t *mac_addr, uint8_t status) {
  Serial.print("\r\nSend message status:\t");
  Serial.println(status == 0 ? "Sent Successfully" : "Sent Failed");
}
 
void setup() {
  // Init Serial Monitor
  Serial.begin(115200);
 
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("There was an error initializing ESP-NOW");
    return;
  }

  // We will register the callback function to respond to the event
  esp_now_register_send_cb(OnSent);
  
  // Add slave
  if (esp_now_add_peer(slaveAddress, ESP_NOW_ROLE_SLAVE, 0, NULL, 0) != 0){
    Serial.println("There was an error registering the slave");
    return;
  }
}

void loop() {
  // Set values to send
  // To simplify the code, we will just set two floats and I'll send it 
  dhtData.temperature = 12.5;
  dhtData.humidity = 58.9;

  // Is time to send the messsage via ESP-NOW
  uint8_t result = esp_now_send(slaveAddress, (uint8_t *) &dhtData, sizeof(dhtData));
   
  if (result == 0) {
    Serial.println("The message was sent sucessfully.");
  }
  else {
    Serial.println("There was an error sending the message.");
  }
  delay(2000);
}
Explicación del código
// Set the SLAVE MAC Address
uint8_t slaveAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

En esta línea configuraremos la dirección MAC del esclavo que recibirá el mensaje. Esta dirección es el formato hexadecimal, pero no te asustes. La dirección MAC que recibiste antes también lo es, por lo que tan sólo tendrás que poner 0x antes de cada grupo de dos cifras. Por ejemplo, la dirección MAC 80:45:32:67:F8:01 se convertiría en {0x80, 0x45, 0x32, 0x67, 0xF8, 0x01}.

// Structure to keep the temperature and humidity data from a DHT sensor
typedef struct temp_humidity {
  float temperature;
  float humidity;
};

// Create a struct_message called myData
temp_humidity dhtData;

Aquí declaramos el struct que contendrá todos los datos que queremos enviar, que recordemos, puede llegar a ocupar 250 bytes. En este ejemplo tan sólo utilizamos como 8 bytes, así que piensa que todavía tienes mucho espacio para enviar datos.

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("There was an error initializing ESP-NOW");
    return;
  }

  // We will register the callback function to respond to the event
  esp_now_register_send_cb(OnSent);
  
  // Add slave
  if (esp_now_add_peer(slaveAddress, ESP_NOW_ROLE_SLAVE, 0, NULL, 0) != 0){
    Serial.println("There was an error registering the slave");
    return;
  }

Esta porción de código es la que más ha cambiado entre el ESP32 y el ESP8266. En el ESP32 definimos un objeto del tipo esp_now_peer_info_t y lo modificamos para que se ajustes a nuestras necesidades. Luego añadimos ese objeto como peer con la función esp_now_add_peer. En el caso del ESP8266 dicho objeto no existe, y le indicamos las opciones directamente en la propia función esp_now_add_peer. Los argumentos son en orden de izquierda a derecha, la dirección MAC de la placa esclava, el role de dicha placa, el canal a usar (0 para usar todos), la clave de encriptación (NULL para no encriptar) y la longitud de la clave de encriptación. La forma de registrar la función CallBack es la misma, pero los argumentos aceptados por esta función cambian.

  // Set values to send
  // To simplify the code, we will just set two floats and I'll send it 
  dhtData.temperature = 12.5;
  dhtData.humidity = 58.9;

  // Is time to send the messsage via ESP-NOW
  esp_err_t result = esp_now_send(slaveAddress, (uint8_t *) &dhtData, sizeof(dhtData));

Por último simplemente rellenamos la nueva variable con los datos que tengamos, que en mi caso me los invento por simplificar el código, y la enviamos a través de la función esp_now_send. Esta parte tampoco cambia.

Placa esclava ESP8266 (destinataria)

La placa esclava al igual que la maestra, necesita unos pocos retoques para hacerla funcional en el ESP8266. Por suerte estos retoques se limitan a los ficheros de cabecera que se usarán, y a la función callback. Esta placa será la que reciba el mensaje y reaccione a él, y en este tipo de comunicación de una dirección, esta placa no podrá iniciar la comunicación y simplemente se limitará a esperar que la placa maestra la inicie y le envíe los datos. En el ejemplo siguiente os muestro cómo recibir estos datos y guardarlos en una variable para posteriormente utilizarlos. Para dicho propósito utilizaremos también una función callback que nos evite tener que estar haciendo pooling para ver si se ha recibido algo.

/*
  Daniel Carrasco
  This and more tutorials at https://www.electrosoftcloud.com/
*/

#include <ESP8266WiFi.h>
#include <espnow.h>

// Structure to keep the temperature and humidity data
// Is also required in the client to be able to save the data directly
typedef struct temp_humidity {
  float temperature;
  float humidity;
};

// Create a struct_message called myData
temp_humidity dhtData;

// callback function executed when data is received
void OnRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) {
  memcpy(&dhtData, incomingData, sizeof(dhtData));
  Serial.print("Bytes received: ");
  Serial.println(len);
  Serial.print("Temperature: ");
  Serial.println(dhtData.temperature);
  Serial.print("Humidity: ");
  Serial.println(dhtData.humidity);
}

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("There was an error initializing ESP-NOW");
    return;
  }
  
  // Once the ESP-Now protocol is initialized, we will register the callback function
  // to be able to react when a package arrives in near to real time without pooling every loop.
  esp_now_register_recv_cb(OnRecv);
}

void loop() {
}

Para no alargar mucho, explicaré las partes del código que difieren al código que ya hemos visto con anterioridad.

// callback function executed when data is received
void OnRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) {
  memcpy(&dhtData, incomingData, sizeof(dhtData));
  Serial.print("Bytes received: ");
  Serial.println(len);
  Serial.print("Temperature: ");
  Serial.println(dhtData.temperature);
  Serial.print("Humidity: ");
  Serial.println(dhtData.humidity);
}

Como podéis ver, los cambios son principalmente en los tipos de variables de los argumentos, pero el resto del código sigue igual. En esta parte definiremos una función que servirá de callback para cuando recibamos datos. Obviamente si recibimos distintos tipos de datos, habrá que comprobar su tipo antes de guardarlo, pero como en el ejemplo sólo recibimos un tipo, simplemente lo copiaremos. Esto se hace con memcpy, que es básicamente una función para realizar la copia del dato recibido en un struct previamente creado. Una vez que hemos copiado el dato, ya podemos trabajar con él sin problemas, que en este casi simplemente lo mandamos por le puerto serial.

  // Once the ESP-Now protocol is initialized, we will register the callback function
  // to be able to react when a package arrives in near to real time without pooling every loop.
  esp_now_register_recv_cb(OnRecv);

Esta función sirve simplemente para registrar la función que será llamada cuando recibamos un mensaje (callback), así que indicaremos la función que hemos creado antes.

Resumen de funciones ESP-Now utilizadas

esp_now_init()

Esta función habla por sí sola, y sirve para inicializar el protocolo ESP-Now. Es importante ejecutarla después de haber configurado el modo del WiFi con WiFi.mode.

esp_now_add_peer()

Esta función permite añadir un cliente utilizando su dirección MAC, lo que lo pondrá en la lista de microcontroladores que recibirán los mensajes. Es básicamente la forma de emparejar las placas… sin ello, no habrá comunicación.

esp_now_send()

Esta función sirve para enviar un mensaje desde la placa maestra a la placa esclava. Se le adjuntará la variable que contiene los datos que queremos enviar, y esta variable podrá ser de cualquier tipo conocido, incluyendo estructuras personalizadas. Es importante que si enviamos una estructura, dicha estructura esté también definida en la placa esclava para que sea capaz de guardar el mensaje en una variable de este tipo y podamos usarla sin problema.

esp_now_register_send_cb()

Con esta función registraremos una función «callback» que será llamada cada vez que se mande un mensaje en la placa maestra. En la placa esclava no funcionará, principalmente porque esta no enviará mensajes. Esta función debe aceptar los argumentos const uint8_t *mac_addr y esp_now_send_status_t status, los cuales son la dirección MAC del destinatario, y el estado del envío respectivamente.

esp_now_register_rcv_cb()

Al igual que la anterior, esta función registra una función «callback», pero que esta vez será llamada cuando se reciban mensajes. Esta función está destinada a las placas esclavas que son las que van a recibir los mensajes de las placas maestras. En este caso esta función deberá ser capaz de aceptar los argumentos const uint8_t * mac, const uint8_t *incomingData e int len, que son la dirección MAC del emisor, los datos que envía y la longitud de los mismos respectivamente.

Probando nuestro sketch ESP-Now

Como has podido ver, crear nuestra comunicación punto a punto con ESP-Now es muy fácil, pero lo mejor es que podremos comunicarnos con placas desde mucha distancia. Ahora vamos a probar el sketch que hemos creado arriba y verás como funciona perfectamente. Para ello necesitarás dos placas ESP para que una sea la placa maestra, y la otra la esclava. Sacaremos la dirección MAC de la placa esclava tal y como hemos explicado al inicio y la configuraremos en el sketch de la placa maestra. Subiremos el sketch como siempre y podremos ver a través de los puertos serie que los datos se están enviando correctamente.

Con esto podremos comprobar que realmente se están enviando los datos.

En las próximas semanas iré añadiendo más tutoriales acerca de este protocolo, como por ejemplo las configuraciones multi-master y multi-esclavos, y como no, la configuración de malla.

Este tutorial se basó en parte en este gran tutorial de randomnerdtutorial:

https://randomnerdtutorials.com/esp-now-esp32-arduino-ide/

Un saludo y espero que os haya gustado.

Daniel Carrasco

DevOps con varios años de experiencia, y arquitecto cloud con experiencia en Google Cloud Platform y Amazon Web Services. En sus ratos libres experimenta con Arduino y electrónica.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.