Maker.io main logo

How To Transmit Messages to Unlimited Receivers in ESP-NOW

2024-07-08 | By Maker.io Staff

How To Transmit Messages to Unlimited Receivers in ESP-NOW

ESP-NOW facilitates effortless data exchange between compatible devices without complicated protocols or setup procedures, but the method comes with a few restrictions. For example, it limits the number of participants, called peers, who can exchange data, and peers have to know the MAC addresses of the other participants. However, this article demonstrates how you can work around these limitations and enable communication between an unlimited number of compatible devices.

Overcoming the ESP-NOW Participant Limit

ESP-NOW allows each device to pair with up to 20 other peers by default. A single sender can transmit to 20 receivers. Similarly, one receiver can process incoming data from up to 20 known senders. However, the broadcast address lets you transmit data to a practically unlimited number of receivers. Multiple senders can transmit broadcast messages simultaneously, meaning that each client can receive information from unlimited transmitters.

This approach introduces some caveats that you should be aware of. Paired devices offer a base level of authentication, as receivers know the addresses of trustworthy senders, which is impossible when utilizing broadcast messages, as they can originate from any source. Similarly, transmitters cannot address receivers specifically. Instead, every participant on the same channel can read all messages.

Messages can include custom sender and recipient fields, and devices can choose to ignore transmissions that originate from untrusted sources or any that are intended for other recipients. Regardless, you should not send sensitive information using broadcast transmissions, even with custom source and destination fields. Clients should treat incoming broadcast messages as non-secure and not perform any critical operations based on broadcast data.

Sending Custom Message Frames in ESP-NOW

This article assumes that you are familiar with ESP-NOW and sending broadcast messages. If you are unfamiliar with ESP-NOW, you should refer to this introductory article that explains all the necessary steps for getting started with the ESP8266 and ESP32. Once they can send and receive data, you can define a custom message format that contains multiple fields:

Copy Code
typedef struct MessageFrame {
    char payload[64];
    unsigned int counter;
    uint8_t senderAddress[6];
    uint8_t receiverAddress[6];
} MessageFrame;

All senders and receivers must know this message format to assemble messages before sending them and reconstruct the original values upon receiving incoming data. Similarly, all devices should store the broadcast address and their own MAC address. In this example, the server contains an additional address that does not exist for testing purposes:

Copy Code
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t myAddress[] = {0xBC, 0xDD, 0xC2, 0xB5, 0xE4, 0xC1};
uint8_t unknownReceiver[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};

The server’s loop method fills in the values of the MessageFrame struct and then sends the data to all clients on the same channel. Note that the server alternates between addressing the message frame to the unknown device and the broadcast address. It further always uses its own address as the message origin. In this example, the payload string doesn’t change between messages. However, the server advances the counter variable with each new message frame it generates:

Copy Code
void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - lastUpdateMillis >= UPDATE_DELAY) {
    MessageFrame message;
    memcpy(message.senderAddress, myAddress, 6);
    if (counter % 2 == 0) {
      memcpy(message.receiverAddress, broadcastAddress, 6);
    } else {
      memcpy(message.receiverAddress, unknownReceiver, 6);
    }
    strncpy(message.payload, "Hello, Client!", PAYLOAD_SIZE);
    message.counter = counter++;
    sendData(message);
    lastUpdateMillis = currentMillis;
  }
}

void sendData(MessageFrame message) {
  uint8_t result = esp_now_send(broadcastAddress, (uint8_t *) &message, sizeof(message));
  if (result != 0) {
    Serial.print("Error sending data!");
  }
}

Decoding Custom Message Frames in ESP-NOW

The server transmits messages to clients using the broadcast MAC address. However, it signs each message frame with its own address, and it may use an address in the recipient field to indicate that the message is intended for a specific peer. Each receiver can read all messages, and it’s up to the peers to respect the value stored in the field and ignore messages intended for others. Therefore, clients should know their MAC address to determine whether a message is for them. Similarly, they must also understand the message format used by the server:

Copy Code
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t myAddress[] = {0x80, 0x7D, 0x3A, 0x78, 0xB6, 0x7C};

typedef struct MessageFrame {
    char payload[64];
    unsigned int counter;
    uint8_t senderAddress[6];
    uint8_t receiverAddress[6];
} MessageFrame;

MessageFrame receivedData;

The client’s setup code is identical to the approach discussed in the Getting Started article. However, the onDataReceived callback is different, and it now utilizes some helper functions:

Copy Code
// ESP8266 version
void onDataReceived(uint8_t *mac_addr, uint8_t *data, uint8_t data_len) {
  memcpy(&receivedData, data, sizeof(receivedData));
  if (processMessage(receivedData)) {
    printMessage(receivedData);
  }
}

bool processMessage(MessageFrame message) {
  bool broadcastMessage = macAddressesEqual(broadcastAddress, message.receiverAddress);
  bool myMessage = macAddressesEqual(myAddress, message.receiverAddress);
  return (myMessage || broadcastMessage);
}

bool macAddressesEqual(uint8_t *expected, uint8_t *actual) {
  for (int i = 0; i < 6; i++) {
    if (actual[i] != expected[i]) {
      return false;
    }
  }
  return true;
}

void printMessage(MessageFrame message) {
  Serial.printf("Sender:   %02X:%02X:%02X:%02X:%02X:%02X\n", message.senderAddress[0], message.senderAddress[1], message.senderAddress[2], message.senderAddress[3], message.senderAddress[4], message.senderAddress[5]);
  Serial.printf("Receiver: %02X:%02X:%02X:%02X:%02X:%02X\n", message.receiverAddress[0], message.receiverAddress[1], message.receiverAddress[2], message.receiverAddress[3], message.receiverAddress[4], message.receiverAddress[5]);
  Serial.printf("Message:  %s\n", message.payload);
  Serial.printf("Counter:  %d\n", message.counter);
  Serial.println("");
}

The callback now copies the received values into the MessageFrame structure and then utilizes the processMessage helper function to determine whether it should ignore the message. This helper compares the recipient MAC address set by the server to the client's and generic broadcast addresses. The server can indicate that all clients should react to a message by writing the broadcast address in the recipient field. If one of those addresses matches, the client proceeds to process the message contents. In this example, it prints the values sent by the server. Note that performing complex operations in the callback function is not good practice. However, for simplicity's sake, I put all message processing in the callback handler.

In either case, the following screenshot shows that the server sends messages to all clients every 2.5 seconds and alternates between addressing them to all clients and the unknown address. The client's output on the right-hand side of the screenshots shows that it ignores every other message and only processes the ones meant for all receivers:

How To Transmit Messages to Unlimited Receivers in ESP-NOW The client only processes messages addressed to the broadcast address.

Summary

You can define a custom message frame format and utilize broadcast transmissions to work around ESP_NOW’s peer limitation. By default, the protocol only supports up to 20 peers at once. However, this approach results in losing the protocol’s built-in authentication based on known peer addresses.

The custom message frame contains sender and recipient fields to indicate the source and destination of messages. Clients can use these fields to determine whether they are the intended message recipient or should ignore it. As it’s up to clients to decide whether or not to ignore messages, servers should never transmit confidential information using this method. As messages can originate from any source, clients should not rely on data received by broadcast transmissions when performing critical operations.

Security issues aside, this approach allows communicating with a practically unlimited number of peers and doesn’t require knowing peer addresses upfront, thus facilitating easy onboarding of new clients.

TechForum

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

Visit TechForum