Close

ESP-Now Introduction

A project log for Hello World for ESP-NOW

A simple example and explanation of how to use ESP-Now for communication between ESP32 devices

Jake WachlinJake Wachlin 03/16/2019 at 19:311 Comment

I'd like to begin this log by noting that I am not a networking expert, so if something I say here is wrong, I'd love to know. At the same time, I know there are many people using the ESP32 who also are not networking experts and just need a working example they can modify to get multiple devices talking to each other. This work examines the example I developed and some of the issues I ran into while making it. The example allows one ESP32 device to blink an LED on another ESP32 device wirelessly, with no configuration (but also no security.) In the GIF below, the ESP32 device on the right acts as a sender, and the ESP32 on the left receives the messages and toggles its LED when it does. Note that "sender" and "receiver" only relate to my example. ESP-NOW supports two-way communication with no such designations.

First, I recommend reading the documentation that Espressif provides about ESP-NOW. It will provide an overview of what ESP-NOW is. If you are not a networking person, a lot of the technical details probably don't mean a lot. But, as builders of things, many of us just want easy to use microcontrollers that can easily talk wirelessly to other microcontrollers. ESP-NOW can be used for that.

My example is built on the concept of sending some arbitrary payload to all ESP32 devices within range, without security. To send to all devices, use the broadcast MAC address, which is all 0xFF.

static uint8_t broadcast_mac[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };

We then define some payload to send. You can change this to whatever you want, as long as it is less than 250 bytes long, since that is the maximum size of the ESP-NOW data body. For my example, I include an address index and a counter. For another project of mine, I plan to build in some ad-hoc message addressing using this address index. The counter here is simply to monitor  how reliable the connection is.

typedef struct __attribute__((packed)) esp_now_msg_t
{
  uint32_t address;
  uint32_t counter;
  // Can put lots of things here...
} esp_now_msg_t;

 My example uses the Arduino ESP32 interface, which makes using the ESP32 very easy. Within the Arduino "setup" routine, I simply set up the Serial connection, the LED, and call my ESP-NOW setup function.

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

  pinMode(LED_PIN,OUTPUT);
  digitalWrite(LED_PIN,LOW);

  network_setup();
}

Within the network_setup function, I initialize  ESP-NOW using Espressifs API, as well as linking callback functions that are called upon sending or receiving of data. First, you must but the device into station mode, and disconnect WiFi (WiFi is likely not connected, but this is here in case it will be brought into someone else's code which uses WiFi also). 

static void network_setup(void)
{
  //Puts ESP in STATION MODE
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();

Next, the "esp_now_init" function is called. This is Espressif's API. My code does not try to recover if it fails, but you may want to act upon the returned value.

  if (esp_now_init() != 0)
  {
    return;
  }

 The next step is critical, at least for how I wanted to use ESP-NOW. The other basic examples I've seen for this either require you to hardcode the MAC addresses of the other devices on your network into the firmware, or share the MAC addresses in a process where each "slave" node enters soft-AP mode, the master connects to it, and receives the MAC address, then switches into ESP-NOW mode. That feels very clunky and could span the air with large numbers of SSIDs only used for configuration. Instead, I set it up so all messages are broadcast to all other devices on the network. You can develop an ad-hoc addressing layer on top if you want. This ensures you can add a device to the network without knowing anything about the other devices.

  esp_now_peer_info_t peer_info;
  peer_info.channel = WIFI_CHANNEL;
  memcpy(peer_info.peer_addr, broadcast_mac, 6);
  peer_info.ifidx = ESP_IF_WIFI_STA;
  peer_info.encrypt = false;
  esp_err_t status = esp_now_add_peer(&peer_info);
  if (ESP_OK != status)
  {
    Serial.println("Could not add peer");
    handle_error(status);
  }

 Finally, we register the callback functions, and then we are done setting up ESP-NOW. It really is that simple.

  // Set up callback
  status = esp_now_register_recv_cb(msg_recv_cb);
  if (ESP_OK != status)
  {
    Serial.println("Could not register callback");
    handle_error(status);
  }

  status = esp_now_register_send_cb(msg_send_cb);
  if (ESP_OK != status)
  {
    Serial.println("Could not register send callback");
    handle_error(status);
  }
}

 Within the main Arduino loop, whichever device you set as the sender simply increments a counter, and sends the message every 2 seconds.

void loop() {
  // Need some delay for watchdog feeding in loop
  delay(2000);

  #ifdef SENDER
  static uint32_t counter = 0;
  esp_now_msg_t msg;
  msg.address = 0;
  msg.counter = ++counter;
  send_msg(&msg);
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  #endif

}

 ESP-NOW also makes sending messages simple, with an easy-to-use API. I wrapped it to handle my custom data better, and to provide some feedback if there are issues. Note that every if no error is seen here, it does not ensure that the other devices actually received it. It simply means the sending device did not have an error during the sending process.

static void send_msg(esp_now_msg_t * msg)
{
  // Pack
  uint16_t packet_size = sizeof(esp_now_msg_t);
  uint8_t msg_data[packet_size];
  memcpy(&msg_data[0], msg, sizeof(esp_now_msg_t));

  esp_err_t status = esp_now_send(broadcast_mac, msg_data, packet_size);
  if (ESP_OK != status)
  {
    Serial.println("Error sending message");
    handle_error(status);
  }
}

The sending callback is called after a message is sent. This is optional, but I am using it here to show any errors that pop up.

static void msg_send_cb(const uint8_t* mac, esp_now_send_status_t sendStatus)
{

  switch (sendStatus)
  {
    case ESP_NOW_SEND_SUCCESS:
      Serial.println("Send success");
      break;

    case ESP_NOW_SEND_FAIL:
      Serial.println("Send Failure");
      break;

    default:
      break;
  }
}

 The receiving callback is also technically optional if you only plan to be sending messages from some device. However, if you do plan to receive data, it is required, and is used to unpack a message. Note that this is called asynchronously, and is likely called from a high priority task. Therefore, do not perform any significant processing of the data here. You should process the data elsewhere. Copying the data into some global data and processing in the main loop is one hacky option that may work, but the better solution would be to use FreeRTOS queues. You can add the data to the queue from this callback, and process it in another task.

static void msg_recv_cb(const uint8_t *mac_addr, const uint8_t *data, int len)
{
  if (len == sizeof(esp_now_msg_t))
  {
    esp_now_msg_t msg;
    memcpy(&msg, data, len);

    Serial.print("Counter: ");
    Serial.println(msg.counter);
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  }
}

I hope this example is useful. If you need a simple way to send arbitrary data payloads between a bunch of devices, and security is not an issue (AKA DO NOT USE for anything serious or for sending sensitive data), then this should work well for you.

Next, I plan to do some latency and reliability testing using the ESP-NOW framework.

Discussions

steve platt wrote 11/30/2019 at 14:08 point

Hi Jake,

Any chance you'll post the entire source package?  I'm (always) worried about introducing errors and gaps when cut/pasting code chunks to rebuild an example..

Thanks,

Steve

  Are you sure? yes | no