Maker.io main logo

How To Implement Two-Way Communication in ESP-NOW

2024-07-10 | By Maker.io Staff

How To Implement Two-Way Communication in ESP-NOW

Previous articles discussed getting started with ESP-NOW, encrypting messages, and overcoming the participant limit in ESP-NOW. All those examples shared the common fact that exclusively one device acts as the server, and all other peers only listen to incoming transmissions without responding. While the single-server approach may be suitable in many cases, some projects require two-way data exchange, and this article illustrates how you can combine the previously discussed techniques to implement multi-sender communication.

Prerequisites

You should familiarize yourself with the ESP-NOW Getting Started guide and ensure your setup is up and running before continuing with the more advanced techniques. Further, you should find the MAC addresses of all devices you want to connect using the following simple sketch that works on ESP32 and ESP8266 boards:

Copy Code
#ifdef ESP32
  // ESP32 Imports
  #include <WiFi.h>
#else
  // ESP8266 Imports
  #include <ESP8266WiFi.h>
#endif
 
void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
}
 
void loop() {
  delay(1000);
  Serial.println(WiFi.macAddress());
}

ESP-NOW supports two-way communication with known peers and all devices on the same channel using the broadcast address. However, if encryption is required, you will have to fall back to using known peers and are, thus, limited to exchanging messages with up to 20 other devices.

Setting Up Two-Way Communication in ESP-NOW

The setup procedure for two-way data exchange in ESP-NOW always follows a similar pattern, regardless of whether messages are sent to known peers or to a broadcast address. However, the required method calls vary slightly depending on the target platform. The following snippet contains the setup procedures for both the ESP32 and ESP8266:

Copy Code
void setup() {
  Serial.begin(9600);
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  esp_now_init();
#ifdef ESP32
  esp_now_register_recv_cb(onDataReceived);
  esp_now_register_send_cb(onDataSent);
  peerInfo.channel = CHANNEL;  
  peerInfo.encrypt = false;
  memcpy(peerInfo.peer_addr, destinationAddress, 6);
  esp_now_add_peer(&peerInfo);
#else
  esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
  esp_now_register_recv_cb(onDataReceived);
  esp_now_register_send_cb(onDataSent);
  esp_now_add_peer(destinationAddress, ESP_NOW_ROLE_COMBO, CHANNEL, NULL, 0);
#endif
}

The setup starts by opening a serial console connection and setting up the WiFi module. These calls are the same for both platforms. However, depending on the target platform, a preprocessor statement splits the code. In the ESP32’s case, the code only registers the callback functions and then sets up the peerInfo struct before adding it to the list of known peers. Using an ESP8266 requires an additional method call that lets the underlying communication service know that this device should act as both a sender and a receiver. The other calls perform the same tasks. However, the method for adding peers doesn’t utilize the peerInfo structure on the ESP8266. Therefore, the ESP32 code also contains an additional variable for storing the peer information:

Copy Code
#define CHANNEL 4
#define UPDATE_DELAY 2500

#ifdef ESP32
  esp_now_peer_info_t peerInfo;
#endif

typedef struct SensorReadings {
    unsigned long counter;
    unsigned long x;
    float y;
} SensorReadings;


uint8_t destinationAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Broadcast

Similar to the setup function, a preprocessor statement excludes the peerInfo variable from the code when compiling it for the ESP8266. Other than that, the code defines a custom structure that holds three values, and the devices use this structure to exchange information. The target MAC address is stored in an array. This example uses the broadcast address to send data to all devices on channel five. However, you can replace the array with up to 20 known addresses. In that case, you must register all peer addresses in the setup function, and when using an ESP32, you must also add one peerInfo struct variable per known peer.

The code also contains additional variables for storing incoming data, keeping track of the last update time, and counting the number of messages sent to clients. Finally, a flag indicates that data was received but not yet processed.

Copy Code
unsigned long lastUpdateMillis = 0UL;
unsigned long counter = 0UL;
bool dataReady = false;
SensorReadings receivedData;

Processing Incoming Data

Each peer executes the callback function registered in the setup method whenever it receives an incoming data transmission. The callback functions don’t differ between the platforms. However, they use slightly different parameter types:

Copy Code
#ifdef ESP32
  void onDataReceived(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
    memcpy(&receivedData, data, sizeof(receivedData));
    dataReady = true;
  }
#else
  void onDataReceived(uint8_t *mac_addr, uint8_t *data, uint8_t data_len) {
    memcpy(&receivedData, data, sizeof(receivedData));
    dataReady = true;
  }
#endif

Irrespective of the platform, the callback function copies the received values to the local receivedData variable defined above and sets the dataReady flag. It’s good practice to keep callback functions as short as possible and to perform tasks that take longer in the main loop of a microcontroller program. In this example, incoming transmissions overwrite previously received data if two transmissions happen to come in in short succession. That’s not a problem in this case, but you should keep that in mind.

Either way, the main loop then processes the received information whenever the dataReady flag is set:

Copy Code
void loop() {
  if (dataReady) {
    Serial.println("Data received!");
    Serial.printf("Counter: %d\n", receivedData.counter);
    Serial.printf("X:       %d\n", receivedData.x);
    Serial.printf("Y:       %f\n\n", receivedData.y);
    dataReady = false;
  }
  unsigned long currentMillis = millis();
  if (currentMillis - lastUpdateMillis >= UPDATE_DELAY) {
    SensorReadings outgoingData;
    outgoingData.counter = counter++;
    outgoingData.x = currentMillis;
    outgoingData.y = (float) rand();
    esp_now_send(destinationAddress, (uint8_t *) &outgoingData, sizeof(outgoingData));
    lastUpdateMillis = currentMillis;
  }
}

This function is the same for both the ESP32 and ESP8266. Whenever the dataReady flag is set, the peer prints the most recently received values stored in the receivedData variable. The last line in the if-block then resets the flag to ensure that data is only processed once.

In addition to processing incoming data in each iteration of the loop, the method sends values to the destination address approximately every 2500 milliseconds. For that, it first creates a new SensorReadings variable and then assigns dummy values. Finally, the code instructs the underlying ESP-NOW service to send the structure to the destination address. In this example, which is the broadcast address. Thus, the message gets transmitted to every other peer on the same channel. You have to issue multiple calls using the individual peer’s addresses if your project requires a more targeted approach.

How To Implement Two-Way Communication in ESP-NOW This screenshot shows the output created by one peer.

The peer sends data to the broadcast address and receives values from other peers.

Handling Outgoing Data Events

While not strictly necessary for data transmissions, you can add a callback function that gets executed whenever information is sent to other peers. Doing so allows peers to react to data transmission errors and handle them. In this example, the code only outputs a short status message in case of successful transmission for debugging purposes:

Copy Code
#ifdef ESP32
  void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
    if (status == ESP_NOW_SEND_SUCCESS) {
      Serial.println("Data sent");
    }
  }
#else
  void onDataSent(uint8_t *mac_addr, uint8_t status) {
    if (status == 0) {
      Serial.println("Data sent");
    }
  }
#endif

Summary

ESP-NOW enables two-way communication between known peers and using the broadcast MAC address. In either case, all devices simultaneously act as senders and receivers. The setup procedure is similar to the single-sender scenario. Peers have to initialize the WiFi hardware and then start the ESP-NOW service. Next, ESP8266-based devices must instruct the service to operate in a two-way communication mode. The peers then register callback functions to handle incoming data and transmission errors. Finally, you need to add all peers a device may want to receive data from or send information to during the setup procedure.

Callback functions play a key role in the ESP-NOW setup. They should be kept as short as possible to ensure efficient data processing. More taxing processing tasks should be implemented in the main program loop. This approach ensures that callback handlers return with minimal delays. Ideally, the handler should only set variable values and flags that inform the loop about new data to process.

TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum