Preview build — page text is a draft awaiting human review. Code badges come from real compiles. The flasher is real but not yet hardware-tested; the Playground is still a preview.
Compile error · ESP32 core 3.x

Fix ESP-NOW “invalid conversion … to 'esp_now_recv_cb_t'” (ESP32 core 3.x)

ESP32 core 3.x changed the ESP-NOW callbacks. Receive gets an esp_now_recv_info_t*; on core 3.3+ send gets a wifi_tx_info_t*. Update the function signatures.

ESP32ESP32-S3ESP32-C3ESP32-C6Updated 3 Nov 2026

Community draft — report a mistake

What you see

OutputOutput
EspNowReceiver.ino: In function 'void setup()':
EspNowReceiver.ino:10:28: error: invalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' {aka 'void (*)(const unsigned char*, const unsigned char*, int)'} to 'esp_now_recv_cb_t' {aka 'void (*)(const esp_now_recv_info*, const unsigned char*, int)'} [-fpermissive]
   10 |   esp_now_register_recv_cb(onRecv);
      |                            ^~~~~~
esp_now.h:172:54: note:   initializing argument 1 of 'esp_err_t esp_now_register_recv_cb(esp_now_recv_cb_t)'
EspNowReceiver.ino:11:28: error: invalid conversion from 'void (*)(const uint8_t*, esp_now_send_status_t)' {aka 'void (*)(const unsigned char*, esp_now_send_status_t)'} to 'esp_now_send_cb_t' {aka 'void (*)(const wifi_tx_info_t*, esp_now_send_status_t)'} [-fpermissive]
exit status 1

ESP-NOW code written for Arduino-ESP32 2.x fails on 3.x because the callback function types changed. C++ won’t convert a function pointer of one shape to another, so you get a long invalid conversion error naming both shapes. Updating the parameter lists is the whole fix:

void onRecv(const esp_now_recv_info_t* info, const uint8_t* data, int len) {
  const uint8_t* mac = info->src_addr;
  // ... use mac, data, len as before
}

void onSent(const wifi_tx_info_t* info, esp_now_send_status_t status) {   // core 3.3+
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "sent" : "failed");
}

Quick fix — try in order

  1. 1

    Change the receive callback

    void onRecv(const uint8_t* mac, const uint8_t* data, int len) becomes void onRecv(const esp_now_recv_info_t* info, const uint8_t* data, int len). The sender’s MAC is now info->src_addr.

  2. 2

    Change the send callback (core 3.3 and newer)

    void onSent(const uint8_t* mac, esp_now_send_status_t status) becomes void onSent(const wifi_tx_info_t* info, esp_now_send_status_t status). Cores 3.0–3.2 still use the old send signature.

  3. 3

    Support several cores with #if

    Wrap the send callback in #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) … #else … #endif, as in the sketch below.

  4. 4

    Don’t cast the old function

    Casting to (esp_now_recv_cb_t)onRecv compiles but your code then reads the info struct as a MAC address — wrong data, possible crashes.

Diagnostic sketch

Verified — core esp32 3.3.12, 3 Nov 2026Compile-only
fix-esp-now-esp32-core3.inoArduino C++Open in PlaygroundOpen in PlaygroundFlash
// ESP-NOW with the callback signatures of Arduino-ESP32 3.x. Flash the same sketch on two
// boards: each broadcasts a counter every 2 s and prints what it receives.
// Receive callback (3.x):  void f(const esp_now_recv_info_t* info, const uint8_t* data, int len)
// Send callback (3.3+):    void f(const wifi_tx_info_t* info, esp_now_send_status_t status)
// Send callback (3.0-3.2): void f(const uint8_t* mac, esp_now_send_status_t status)
#include <WiFi.h>
#include <esp_now.h>

const uint8_t BROADCAST[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint32_t counter = 0;

void onRecv(const esp_now_recv_info_t* info, const uint8_t* data, int len) {
  const uint8_t* m = info->src_addr;  // the sender's MAC moved into info
  uint32_t value = 0;
  if (len == sizeof value) memcpy(&value, data, sizeof value);
  Serial.printf("From %02X:%02X:%02X:%02X:%02X:%02X  value %lu\n", m[0], m[1], m[2], m[3], m[4], m[5], (unsigned long)value);
}

#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
void onSent(const wifi_tx_info_t* info, esp_now_send_status_t status) {
#else
void onSent(const uint8_t* mac, esp_now_send_status_t status) {
#endif
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "sent" : "send failed");
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    Serial.println("esp_now_init failed");
    return;
  }
  esp_now_register_recv_cb(onRecv);
  esp_now_register_send_cb(onSent);
  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, BROADCAST, 6);
  peer.channel = 0;  // current Wi-Fi channel
  peer.encrypt = false;
  esp_now_add_peer(&peer);
  Serial.printf("My MAC: %s\n", WiFi.macAddress().c_str());
}

void loop() {
  counter++;
  esp_now_send(BROADCAST, (const uint8_t*)&counter, sizeof counter);
  delay(2000);
}

Why it happens

Why did the callbacks change?

ESP-IDF 5 passes more information (sender and destination address, RSSI in rx_ctrl) to the receive callback, and IDF 5.5 does the same for the send callback. Arduino-ESP32 3.x exposes the IDF types directly.

Most ESP-NOW tutorials show the old form

They were written for core 2.x. The rest of the code (esp_now_init, add_peer, send) is unchanged.

Getting the RSSI

With the new receive callback, info->rx_ctrl->rssi gives the signal strength of each packet — useful for range tests.

Board-specific notes

ESP32: Both boards must be on the same Wi-Fi channel. With one board also joined to a router, ESP-NOW uses the router’s channel.

ESP8266: Uses its own espnow.h API with the old-style callbacks; it can talk to an ESP32 but the code differs.

↑↓ to move · Enter to open