ESP_NOW V2.0 ESP32 to ESP32 problem

After many hours of searching I have finaly found a sketch that compiles and works using V2.0 of ESP_NOW for an ESP32 to ESP32.

There are lots of online examples including the one on the Arduino pages that will not compile.

The example below sends a message from a master ESP32 to a slave ESP32.

What I want to do is send a reading from a sensor and not the text message in the example but I have tried in vain to find the correct way of doing this.

I can read the sensor and put it in a ‘float’ but I cant find a way of sending the value.

I have tried using lines from various different online examples but all I get is compile errors.


#include "ESP32_NOW.h"
#include "WiFi.h"

#include <esp_mac.h>  // For the MAC2STR and MACSTR macros

#define ESPNOW_WIFI_CHANNEL 6

class ESP_NOW_Broadcast_Peer : public ESP_NOW_Peer {
public:
  // Constructor of the class using the broadcast address
  ESP_NOW_Broadcast_Peer(uint8_t channel, wifi_interface_t iface, const uint8_t *lmk) : ESP_NOW_Peer(ESP_NOW.BROADCAST_ADDR, channel, iface, lmk) {}

  // Destructor of the class
  ~ESP_NOW_Broadcast_Peer() {
    remove();
  }

  // Function to properly initialize the ESP-NOW and register the broadcast peer
  bool begin() {
    if (!ESP_NOW.begin() || !add()) {
      log_e("Failed to initialize ESP-NOW or register the broadcast peer");
      return false;
    }
    return true;
  }

  // Function to send a message to all devices within the network
  bool send_message(const uint8_t *data, size_t len) {
    if (!send(data, len)) {
      log_e("Failed to broadcast message");
      return false;
    }
    return true;
  }
};

/* Global Variables */

uint32_t msg_count = 0;

// Create a broadcast peer object
ESP_NOW_Broadcast_Peer broadcast_peer(ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, nullptr);

/* Main */

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

  // Initialize the Wi-Fi module
  WiFi.mode(WIFI_STA);
  WiFi.setChannel(ESPNOW_WIFI_CHANNEL);
  while (!WiFi.STA.started()) {
    delay(100);
  }

  Serial.println("ESP-NOW Example - Broadcast Master");
  Serial.println("Wi-Fi parameters:");
  Serial.println("  Mode: STA");
  Serial.println("  MAC Address: " + WiFi.macAddress());
  Serial.printf("  Channel: %d\n", ESPNOW_WIFI_CHANNEL);

  // Register the broadcast peer
  if (!broadcast_peer.begin()) {
    Serial.println("Failed to initialize broadcast peer");
    Serial.println("Reebooting in 5 seconds...");
    delay(5000);
    ESP.restart();
  }

  Serial.printf("ESP-NOW version: %d, max data length: %d\n", ESP_NOW.getVersion(), ESP_NOW.getMaxDataLen());

  Serial.println("Setup complete. Broadcasting messages every 5 seconds.");
}

void loop() {
  // Broadcast a message to all devices within the network
  char data[32];
  snprintf(data, sizeof(data), "Hello, World! #%lu", msg_count++);

  Serial.printf("Broadcasting message: %s\n", data);

  if (!broadcast_peer.send_message((uint8_t *)data, sizeof(data))) {
    Serial.println("Failed to broadcast message");
  }

  delay(5000);
}

Try these:

Sender:

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

uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //here goes the MAC adress of the receiver ESP
int contador = 0;

typedef struct struct_message {
    float comando;
} struct_message;

struct_message myData;


esp_now_peer_info_t peerInfo;


void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("I sent: ");
  Serial.println(myData.comando);
}
 
void setup() {
 
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);

  if (esp_now_init() != ESP_OK) {
    Serial.println("Initializing ESP-NOW failed");
    return;
  }

  esp_now_register_send_cb(esp_now_send_cb_t(OnDataSent));
  
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  
       
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add receiver");
    return;
  }
}
 
void loop() {
    esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
}

Receiver:

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


typedef struct struct_message {
    float comando;
} struct_message;

struct_message myData;

 
// Callback function executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t * incomingData, int len) {
  memcpy(&myData, incomingData, sizeof(myData));
  Serial.print("I received: ");
  Serial.println(myData.comando);
}

 
void setup() {

  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  

  if (esp_now_init() != ESP_OK) {
    Serial.println("Fail to initialize ESP-NOW");
    return;
  }
  esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
  delay(2000);
}
 
void loop() {}

This is the line that puts the data to be sent in an array of chars

    snprintf(data, sizeof(data), "Hello, World! #%lu", msg_count++);

This puts the text "Hello World!" into the array followed by the value of msg_count

It then broadcasts what is in the array using this section of code

  if (!broadcast_peer.send_message((uint8_t *)data, sizeof(data)))
    {
        Serial.println("Failed to broadcast message");
    }

In order to send a float you need to put the value of the float in the array instead of the text. If you have the value in a global float variable named sensorValue then you could do this

 snprintf(data, sizeof(data), "Hello, World! %f", sensorValue);

and use the same code to send it

Start by trying that then you can refine it. You can control how many decimal places are sent, for example, and you do not need to send the text

Once you receive the float you can move on to parsing and using its value but get the basics working first

Thank you this is exactly what I was looking for apart from my original sketch sent to any boards that were ‘listening’

With your sketch can I send to different boards at the same time using different MAC addresses?

The sketch posted by @Brazilino send the same data to all Arduinos at the same time by using the broadcast MAC address. Is that what yiu want or do you want to specify different data to be sent to specific boards ?

Take a look at this page for a few examples of what is possible

These are the examples that will not compile with V2 of ESP_NOW

The example by @Brazilino asks for a MAC address for the receiving ESP32. Does this not mean that I would need the MAC addresses for any other receiver boards?

The transmitter sketch posted by @Brazilino declares this as the MAC address to send to

uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };  //here goes the MAC adress of the receiver ESP

It is then used when sending a message

    esp_now_send(broadcastAddress, (uint8_t *)&myData, sizeof(myData));

The MAC address used is not specific to a receiver, rather, it is a broadcast address. Any ESP-NOW receiver listening for a message can receive it and it is not necessary to know the MAC address of the receiver

If data is to be sent to a specific MAC address then it must be used in the code of the transmitter

That is a shame. When I get time I will take a look at the problems

Hmm, ESP-NOW (didn't it used to be capitalized ESP-Now?) v2.0 first released with
esp32 board Arduino Release v3.2.1 based on ESP-IDF v5.4.2 back in July. Compared to the most recent IDF v5.3, a few months prior:

$ git diff v5.3.3 v5.4.2 -- components/esp_wifi/include/esp_now.h
diff --git a/components/esp_wifi/include/esp_now.h b/components/esp_wifi/include/esp_now.h
index e0261375bb..ddc1f9efae 100644
--- a/components/esp_wifi/include/esp_now.h
+++ b/components/esp_wifi/include/esp_now.h
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD
+ * SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD
  *
  * SPDX-License-Identifier: Apache-2.0
  */
@@ -51,6 +51,7 @@ extern "C" {
 
 #define ESP_NOW_MAX_IE_DATA_LEN      250       /**< Maximum data length in a vendor-specific element */
 #define ESP_NOW_MAX_DATA_LEN  ESP_NOW_MAX_IE_DATA_LEN   /**< Maximum length of data sent in each ESPNOW transmission for v1.0 */
+#define ESP_NOW_MAX_DATA_LEN_V2      1470      /**< Maximum length of data sent in each ESPNOW transmission for v2.0 */
 
 /**
  * @brief Status of sending ESPNOW data .
@@ -130,10 +131,12 @@ esp_err_t esp_now_init(void);
 esp_err_t esp_now_deinit(void);
 
 /**
-  * @brief     Get the version of ESPNOW. Currently, ESPNOW supports one version: v1.0.
+  * @brief     Get the version of ESPNOW. Currently, ESPNOW supports two versions: v1.0 and v2.0.
   *
-  *            The v1.0 devices can receive packets if the packet length is less than or equal to ESP_NOW_MAX_IE_DATA_LEN.
-  *            For packets exceeding this length, the v1.0 devices will discard the packet entirely.
+  *            The v2.0 devices are capable of receiving packets from both v2.0 and v1.0 devices. In contrast, v1.0 devices can only receive packets from other v1.0 devices.
+  *            However, v1.0 devices can receive v2.0 packets if the packet length is less than or equal to ESP_NOW_MAX_IE_DATA_LEN.
+  *            For packets exceeding this length, the v1.0 devices will either truncate the data to the first ESP_NOW_MAX_IE_DATA_LEN bytes or discard the packet entirely.
+  *            For detailed behavior, please refer to the documentation corresponding to the specific IDF version.
   *
   * @param     version  ESPNOW version
   *

The main functional change is the addition of the ESP_NOW_MAX_DATA_LEN_V2 macro, which indicates the larger supported payload size. And esp_now_get_version can now return two possible values.

The bigger breaking change was with the release of esp32 board v3 two years ago; which also added the ESP_NOW Arduino library, represented by <ESP32_NOW.h> (I'm old school and use the IDF <esp_now.h>): Arduino Release v3.0.0 based on ESP-IDF v5.1.4. The last v2 was Arduino Release v2.0.17 based on ESP-IDF v4.4.7, so that was also a major version jump in IDF. That breaking change was

 /**
   * @brief     Callback function of receiving ESPNOW data
-  * @param     mac_addr peer MAC address
+  * @param     esp_now_info received ESPNOW packet information
   * @param     data received data
   * @param     data_len length of received data
+  * @attention esp_now_info is a local variable,it can only be used in the callback.
   */
-typedef void (*esp_now_recv_cb_t)(const uint8_t *mac_addr, const uint8_t *data, int data_len);
+typedef void (*esp_now_recv_cb_t)(const esp_now_recv_info_t * esp_now_info, const uint8_t *data, int data_len);

replacing a pointer to the sending MAC with a new info struct

+/**
+ * @brief ESPNOW packet information
+ */
+typedef struct esp_now_recv_info {
+    uint8_t * src_addr;                      /**< Source address of ESPNOW packet */
+    uint8_t * des_addr;                      /**< Destination address of ESPNOW packet */
+    wifi_pkt_rx_ctrl_t * rx_ctrl;            /**< Rx control info of ESPNOW packet */
+} esp_now_recv_info_t;

containing both the sender/source and destination. So now you can tell if the message was directed to your MAC specifically, or an all-FF broadcast.

Which is a good time to mention that ESP-NOW receivers should be prepared to receive messages not actually intended for them. So don't blindly read the packet and act upon its contents. Some verification is required. It could go as far as embedding some kind of signature in the payload.

That's the old API, but then this (hard-to-see function-style) cast

forces it to work. Also, C++ automatically puts struct names in the main namespace, so this

could be

struct struct_message {
    float comando;
} myData;
1 Like

Thanks @kenb4 .

Well, the truth is that many of us who are not that fluent in C++ sweep the internet looking for examples that we can use as a base for our own code.

What @B1Gtone said in the OP once happens to me when I used Randon Nerd Tutorials code as a base. Then I found

on The Drone Bot Workshop site and this solved the problem by my side. I wasn’t aware of the reasons why it did work until you bring some light on it.

I’ve been trying to help others in using ESP-Now since I found my solution.

If you don’t mind helping us a little more… besides this change:

What other changes would you suggest to improve the sender and receiver codes?

If all you are sending is a single float why use a struct ?

I just kept the structure in order to make it easier for anyone to see that other data can be part of it.

That makes sense now. I tried with the 0xff, 0xff etc and it worked. I also tried by entering the MAC address of one board and that worked as well.

@Brazilino

Can your supplied code be simply changed for use with ESP8266 boards?

I know you need to use espnow.h and not esp_now.h but are there many more changes required or is a completely different aproach?

I never tried it with ESP8266, so I’m not sure.

Same approach, but several changes. Here is a sketch that works with either ESP8266 or ESP32, using conditional compilation -- the usual C/C++ skullduggery

#if __has_include(<espnow.h>)
  #define ESP_NOW_BACK_THEN
  #pragma message "ESP8266-NOW"
  #include <ESP8266WiFi.h>
  #include <espnow.h>
  #define ESP_OK 0  // not defined for ESP8266
  #define WIFI_MODE_STA WIFI_STA  // no "MODE" on ESP8266
#else
  #pragma message "ESP32-NOW"
  #include <WiFi.h>
  #include <esp_wifi.h>  // just for esp_wifi_set_channel()
  #include <esp_now.h>
  #include <MacAddress.h>  // to make it easier to print those
#endif

// Either everyone connects to the same WiFi Access Point
//   - that puts everyone on the same channel
//   = then you can disconnect if you're not going to use normal WiFi
// or you can hard-code a channel
#define WIFI_CHANNEL 6  // the "middle" channel; coincidentally same number of octets in a MAC address

uint8_t target_mac[6];  // don't feel like typing six FFs and five commas
bool switch_to_found_peer = true;  // used by ESP8266 only

constexpr uint64_t payload_id{ 0xACE0FBA5E00FF1CE };

struct NOW_Payload {
  uint64_t signature{ payload_id };
  unsigned long millis;
} payload;

#ifdef ESP_NOW_BACK_THEN
void OnDataSent(uint8_t *des_addr, uint8_t status) {
#else
void OnDataSent(const uint8_t *des_addr, esp_now_send_status_t status) {
#endif
  static bool wantToSeeAllOfThem = false;
  if (wantToSeeAllOfThem || status != ESP_OK) {
    Serial.print("OnDataSent: \040\040\040\040\040");
    Serial.print(millis());
    Serial.print('\t');
    Serial.println(status);
  }
}

void printMac(const uint8_t *mac) {
#ifdef ESP_NOW_BACK_THEN
  char macStr[18];
  sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
#else
  MacAddress macStr{ mac };
#endif
  Serial.print(macStr);
}

#ifdef ESP_NOW_BACK_THEN
void OnDataRecv(uint8_t *src_addr, uint8_t *data, uint8_t data_len) {
  if (switch_to_found_peer) {
    auto err = esp_now_add_peer(src_addr, ESP_NOW_ROLE_COMBO, WIFI_CHANNEL, nullptr, 0);
    if (err) {
      Serial.println("failed to add found-peer");
    } else {
      memcpy(target_mac, src_addr, sizeof(target_mac));
      Serial.println("switched to found-peer");
    }
    switch_to_found_peer = false;
  }
#else
void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len) {
  const uint8_t *src_addr = esp_now_info->src_addr;
#endif
  NOW_Payload pay;
  if (data_len != sizeof(pay)) {
    Serial.print("NOW received ");
    Serial.print(data_len);
    Serial.println(" foreign bytes");
    return;
  }
  memcpy(&pay, data, sizeof(pay));
  if (pay.signature != payload_id) {
    Serial.print("NOW received signature that does not match: ");
    Serial.println(pay.signature, HEX);
    return;
  }
  Serial.print("NOW received from ");
  printMac(src_addr);
  Serial.print(" sent at their ");
  Serial.println(pay.millis);
}

void setup() {
  Serial.begin(115200);
  for (auto &b : target_mac) b = 0xFF;  // start with broadcast

  WiFi.mode(WIFI_MODE_STA);
  if (esp_now_init() == ESP_OK) {
#ifdef ESP_NOW_BACK_THEN
    Serial.println("ESP-NOW initialized");
#else
    uint32_t version;  // 32 bits for either 1 or 2
    esp_now_get_version(&version);
    Serial.print("ESP-NOW initialized, version: ");
    Serial.print(version, HEX);
#endif
  } else {
    for (;;) {
      Serial.println("Initializing ESP-NOW failed");
      delay(3000);
    }
  }

  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);

#ifdef ESP_NOW_BACK_THEN
  auto err = esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
  if (err) {
    for (;;) {
      Serial.print("esp_now_set_self_role failed: ");
      Serial.println(err);
      delay(3000);
    }
  }
  err = esp_now_add_peer(target_mac, ESP_NOW_ROLE_COMBO, WIFI_CHANNEL, nullptr, 0);
#else
  esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
  esp_now_peer_info_t peerInfo{};  // zero-init all fields
  // peerInfo.channel = 0;  // or WIFI_CHANNEL
  // peerInfo.encrypt = false;
  memcpy(peerInfo.peer_addr, target_mac, sizeof(target_mac));
  auto err = esp_now_add_peer(&peerInfo);
#endif
  if (err) {
    for (;;) {
      Serial.print("esp_now_add_peer failed: ");
      Serial.println(err);
      delay(3000);
    }
  }
}

void loop() {
  payload.millis = millis();
  auto err = esp_now_send(target_mac, reinterpret_cast<uint8_t *>(&payload), sizeof(payload));
  Serial.print("esp_now_send at: ");
  Serial.print(payload.millis);
  if (err) {
    Serial.print(" failed: ");
    Serial.println(err);
    delay(3500);
  } else {
    Serial.println();
  }
  delay(random(750, 1750));
}

First, Instead of looking for another macro that indicates ESP8266, __has_include worked to find espnow.h with no underscore. In doing so, define a custom empty macro that indicates it's the old style, to be used later. You also need WiFi for both, and a couple more for ESP32.

Next, note the difference in function signatures for the send callback

#ifdef ESP_NOW_BACK_THEN
void OnDataSent(uint8_t *des_addr, uint8_t status) {
#else
void OnDataSent(const uint8_t *des_addr, esp_now_send_status_t status) {
#endif

On ESP32, the destination MAC is through a pointer-to-const; and the status is a specific type.

It's easy to print MAC addresses on ESP32.

Also pointers-to-const on the receive callback. More importantly, as mentioned earlier the first argument is no longer a simple pointer to the sender's MAC. But that is easily extracted. Will talk about switch_to_found_peer later.

#ifdef ESP_NOW_BACK_THEN
void OnDataRecv(uint8_t *src_addr, uint8_t *data, uint8_t data_len) {
  if (switch_to_found_peer) {
    auto err = esp_now_add_peer(src_addr, ESP_NOW_ROLE_COMBO, WIFI_CHANNEL, nullptr, 0);
    if (err) {
      Serial.println("failed to add found-peer");
    } else {
      memcpy(target_mac, src_addr, sizeof(target_mac));
      Serial.println("switched to found-peer");
    }
    switch_to_found_peer = false;
  }
#else
void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len) {
  const uint8_t *src_addr = esp_now_info->src_addr;
#endif

The init code differs only because ESP32 now supports two versions of ESP-NOW

#ifdef ESP_NOW_BACK_THEN
    Serial.println("ESP-NOW initialized");
#else
    uint32_t version;  // 32 bits for either 1 or 2
    esp_now_get_version(&version);
    Serial.print("ESP-NOW initialized, version: ");
    Serial.print(version, HEX);
#endif

With the correct function signatures, the callback registration is straightforward

  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);

The biggest difference might be with the ESP-NOW setup. ESP8266 requires a call to _set_self_role. Then _add_peer requires a role and a channel. (The last two arguments -- empty in this case -- are supposedly for encryption.)

#ifdef ESP_NOW_BACK_THEN
  auto err = esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
  if (err) {
    for (;;) {
      Serial.print("esp_now_set_self_role failed: ");
      Serial.println(err);
      delay(3000);
    }
  }
  err = esp_now_add_peer(target_mac, ESP_NOW_ROLE_COMBO, WIFI_CHANNEL, nullptr, 0);
#else
  esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
  esp_now_peer_info_t peerInfo{};  // zero-init all fields
  // peerInfo.channel = 0;  // or WIFI_CHANNEL
  // peerInfo.encrypt = false;
  memcpy(peerInfo.peer_addr, target_mac, sizeof(target_mac));
  auto err = esp_now_add_peer(&peerInfo);
#endif

On ESP32, you first set the "base" WiFi channel to match the peers. So even though each peer info contains a channel, it doesn't seem like those can vary. A common way to get everyone on the same channel is for them to join the same Access Point, and then disconnect if that's not actually needed.

Those are the functional differences.

Does this actually work though? For me, ESP8266 receives most of the messages from ESP32; but vice-versa, ESP32 receives almost none. Two ESP32 work OK. I don't have a second ESP8266 to try.

For a broadcast to all-FF, the send callback never indicates an error. On ESP8266, I added switch_to_found_peer so that in the receive callback, it could try transmitting directly. In that case, OnDataSent accurately reports it almost never worked. Unfortunately, no more details on that; even on ESP32, the status type is just

typedef enum {
    ESP_NOW_SEND_SUCCESS = 0,       /**< Send ESPNOW data successfully */
    ESP_NOW_SEND_FAIL,              /**< Send ESPNOW data fail */
} esp_now_send_status_t;

BTW, OnDataRecv does implement a "magic number" check -- in this case, a bit of hex speak -- to verify the payload is of the expected kind.

1 Like