ESP32: WiFi y ESP-Now de forma simultánea

Si como yo acabas de empezar a trabajar con el protocolo WiFi ESP-Now, seguramente habrás utilizado el ejemplo básico para probar. Este ejemplo seguramente será parecido al que comento en mi guía básica de ESP-Now. Si tras haber puesto en funcionamiento la comunicación ESP-Now te has encontrado con que querías usar el WiFi y ESP-Now de forma simultánea, seguramente te habrá fallado. Al conectar el WiFi la mayoría de los paquetes ESP-Now se pierden o no llegan en absoluto. Esto parece ser que está relacionado con el modo en el que funciona el WiFi, por lo que en esta guía os enseñaré como solucionarlo.

Comencemos con el ejemplo básico de WiFi y ESP-Now, el cual ya sabemos que no funciona.

Maestro WiFi y ESP-Now no funcional

He recalcado lo de no funcional, para que luego no digáis que no os lo avisé o que no funciona. Este ejemplo, es el que usaríamos si mezcláramos una conexión WiFi estándar con el ejemplo de protocolo ESP-Now, el cual nos daría pérdida de paquetes. El maestro es el nodo que enviará los datos ESP-Now al esclavo, que será el que se encargará de conectar al WiFi para hacer lo que queramos con ellos. Este nodo no se conectará al WiFi, por lo que tan sólo le usaremos para enviar.

/*
  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);
}

Esclavo no funcional

Arriba hemos visto el sketch básico de un maestro ESP-Now que será el que recolecte los datos y se los envíe al esclavo por ejemplo. El esclavo será el nodo que se conecte al WiFi para poder enviar los datos por internet y otras tareas que deseemos. Es en este nodo en el que nos encontraremos el problema de que no llegan los paquetes y por lo tanto perdemos los datos. El sketch de ejemplo es el siguiente:

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

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

static const char* ssid     = "WIFI_SSID";
static const char* password = "WIFI_PASSWORD";

// 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);

  WiFi.begin(ssid, password);

  //check wi-fi is connected to wi-fi network
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  
  // 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() {
}

Resultado

Si probaste los ejemplos de arriba, habrás podido observar que no llegan los paquetes desde el maestro ESP-Now al esclavo una vez que conectas el WiFi, y si comentas la línea WiFi.begin() el error desaparece.

De forma aleatoria pueden llegar algunos paquetes, como los de esta captura de abajo en la cual tras un montón de intentos por parte del maestro, han llegado 4 paquetes al esclavo.

Si tu aplicación no es crítica quizá no te importe reintentar hasta que llegue, pero personalmente me parece hacer mucho ruido y pocas nueces. Además de que personalmente lo veo como un parche feo y no es lo correcto.

Solución fácil WiFi y ESP-Now

A pesar de lo complicado que me ha sido conseguir encontrar la solución, navegando por foros con ejemplos que no funcionaban, gente diciendo que había que usar dos placas, una para el WiFi y otra para el ESP-Now conectadas por el puerto serial… y resulta que la solución es de lo más fácil.

El problema principal parece estar provocado porque el WiFi en modo estación pasa al modo de descanso mientras no tiene trabajo. Esto hace que no escuche para recibir los paquetes ESP-Now y por lo tanto se pierdan. Para solucionarlo tendremos que forzar a nuestro microcontrolador a que escuche continuamente, y esto se consigue convirtiéndolo en un AP (Punto de acceso). Tranquilo, no hará falta ni exponer el microcontrolador, sólo decirle que se configure como AP y Station al mismo tiempo. Para ello cambiaremos esta línea:

WiFi.mode(WIFI_STA);

Por esta otra:

WiFi.mode(WIFI_AP_STA);

Con lo que el código final te quedaría así:

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

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

static const char* ssid     = "WIFI_SSID";
static const char* password = "WIFI_PASSWORD";

// 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);

  WiFi.begin(ssid, password);

  //check wi-fi is connected to wi-fi network
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  
  // 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() {
}

Y con este simple cambio, solucionas un problema que parece haber traído de cabeza a muchos por internet. No digo que la gente sea muy torpe, quizá cuando ellos lo intentaron fallaba o no estaba implementado. Por ello os traigo esta forma fácil con la esperanza de que les sirva a muchos.

Solución difícil

Si te falló la opción de arriba, hay una opción alternativa que es poner el canal del WiFi en el maestro, lo cual parece ser también un requisito para que funcione. En mi caso sin ir más lejos, a la hora de publicar la primera parte de esta guía me funcionó perfectamente, pero al ir a implementarlo dos días más tarde en mi proyecto comenzaba a fallar. Quizá fuera un cambio de canal por parte de mi router o similar, por lo que decidí ampliar esta guía con la otra opción que había visto.
Que no te asuste el título, lo de solución difícil es porque hay que añadir unas líneas más, pero no es difícil realmente. las líneas a añadir son la cabecera para poder configurar el canal y la línea en la que se indica el canal.

... // Another headers and stuff
#include <esp_wifi.h>
... // The rest of stuff

setup() {
  ... // More of your code
  int32_t channel = 11;
  esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
  ... // Rest of your setup code
}

// The rest of code

Tras estos ajustes debería funcionarte, pero si en tu caso no conoces el canal o te pasa como a mí que es automático, quizá te venga mejor detectar el canal de tu router de forma automática. Para ello puedes usar este código:

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
      for (uint8_t i=0; i<n; i++) {
          if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
              return WiFi.channel(i);
          }
      }
  }
  return 0;
}

El cual pasándole el SSID de tu router devuelve el canal actual. Gracias a RandomNerdTutorials que me ayudo con esta última solución.

Con ambos cambios el código final quedaría así:

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

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

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

// Insert your SSID
constexpr char WIFI_SSID[] = "YOUR_WIFI_SSID";

// 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");
}

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
      for (uint8_t i=0; i<n; i++) {
          if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
              return WiFi.channel(i);
          }
      }
  }
  return 0;
}
 
void setup() {
  // Init Serial Monitor
  Serial.begin(115200);
 
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  int32_t channel = getWiFiChannel(WIFI_SSID);
  esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);

  // 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);
}

Con estos cambios ya deberías tener una comunicación WiFi y ESP-Now perfecta y sin problemas.

Como siempre, espero que os haya gustado y os sirva, y no dudéis en comentar o compartir. ¡Un saludo!.

7 comentarios en «ESP32: WiFi y ESP-Now de forma simultánea»

  1. Hola, muchas gracias por el tutorial. Mi pregunta se refiere a lo siguiente: para que usar un router externo si el propio esp8266 puede hacer las veces de router?
    Funcionaría esp now si el esp8266 opera como sta y ap?

    Responder
    • Hola Carlos,

      El uso de un router es el enfoque estándar, y es por eso que me basé en este método de uso.
      Respecto a usar un ESP8266 como AP no creo que fuese necesario, ya que el propio protocolo ESP-NOW permite contactar con otros clientes sin AP y por lo tanto no aportaría gran cosa. Esta configuración la hice como experimento para un proyecto en el que varios ESP32 con sensores se conectaban a uno común a través de ESP-NOW (útil para zonas donde el router no llegaba), y usar este ESP32 central para ver o enviar los datos de esos sensores. Por falta de tiempo no llegué a terminarlo y ahora estoy intentando usar MQTT e intentaré usar Wifi Mess para la conectividad.

      Un saludo.

      Responder
  2. Hola Daniel! Una consulta, es posible conectar con ESP-NOW y Bluetooth simultaneamente. Esto pensando en un proyecto donde un ESP transmite a otro ESP (mediante ESP-NOW) y este último envía datos por bluetooth como un teclado por ejemplo. ¿Es posible hacerlo de esta manera? Saludos!

    Responder
    • Hola José,

      La verdad es que es un buen experimento a probar y bastante útil, pero me temo que nunca he llegado a probarlo. En un principio entiendo que no debería haber problema en hacerlo, lo único que el chip usado para ambas comunicaciones es el mismo y por lo tanto es complicado hacerlo funcionar correctamente. Ya lo es sólo usando Wifi y Bluetooth, así que si añadimos ESP-NOW.

      Un saludo

      Responder

Deja un comentario

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