iLabs ESP-NOW
ESP-NOW is a connectionless 2.4GHz protocol from Espressif. It lets small packets travel directly between devices without a WiFi access point and without any TCP/IP stack. This is useful for sensor networks, remote controls and other cases where you only need to move a few bytes now and then with low latency.
The RP2040 and RP2350 do not have a radio of their own. On the iLabs
Challenger WiFi boards the radio is a separate ESP32-C6 or ESP32-C3
co-processor sitting next to the host processor. This library gives you the
Arduino ESP-NOW API on the host and talks to the co-processor over a UART
link, using the iLabs AT+EN command set. The co-processor must be flashed
with the iLabs ESP-NOW interpreter firmware for this to work.
The programming surface is the same ESP_NOW object and ESP_NOW_Peer
class as in the arduino-esp32 core. A sketch written for an ESP32 therefore
builds against this library with only the radio bring-up changed, as described
below.
RP2040 / RP2350 host ESP32-C6 co-processor
(this Arduino library) (AT+EN interpreter)
| |
| UART (ESP_SERIAL_PORT) |
| ESP_NOW.begin() --> AT+ENINIT -->| ESP-NOW
| peer.send() --> AT+ENSEND -->| ))) 2.4GHz ((( peers
| ESP_NOW.poll() <-- +ENRECV -----|
Supported boards
ESP-NOW is available on the iLabs boards that carry an ESP32 co-processor:
Challenger 2040 WiFi
Challenger 2040 WiFi/BLE
Challenger 2040 WiFi6/BLE
Challenger NB 2040 WiFi
Connectivity 2040 LTE/WiFi/BLE
RPICO32
Challenger 2350 WiFi6/BLE5
On any other board the library will not build. The reason is that it needs a board variant that wires up the ESP32 UART, and that is only true for the boards above.
Selecting the ESP-NOW firmware type
Pick the board in the IDE, open the Tools menu and set ESP Wifi Type to
ESP-NOW. This defines ILABS_ESPNOW for the build. The default value
is ESP AT, so a board keeps its normal AT firmware behaviour until you
change the menu. On the two boards that also offer ESP Hosted, ESP-NOW is
simply added as a third choice.
When ESP-NOW is selected the board variant no longer resets the ESP32 during start-up. The library takes over the reset sequence instead, so that it can wait for the co-processor to report that it is ready. See Board reset handling below.
If you build with PlatformIO the Tools menu is not available. Add the define
yourself in platformio.ini:
build_flags = -DILABS_ESPNOW
Differences from an ESP32 sketch
Because the radio is a co-processor over a UART, and not a radio inside the main chip, two things change compared to a normal ESP32 ESP-NOW sketch.
First, the radio bring-up. On an ESP32 you would start WiFi and set the channel. Here you call one function instead. The library already knows which UART the board variant uses, so the sketch never names a serial port:
ESP_NOW.setLink(channel); // opens the ESP32 UART and resets the co-processor
Second, the servicing of incoming packets. Call ESP_NOW.poll() once every
time through loop(), in the same way you would call ArduinoOTA.handle().
Received frames are handed to the peer callbacks from inside poll(). Keep
loop() free of long blocking calls. On the RP2040 a plain delay() does
not yield, so a long delay() will hold up the receive path. For work that
must happen at a fixed interval, use a timer to raise a flag and do the sending
from loop() when the flag is set.
Everything else stays the same as in the arduino-esp32 API. Peer subclassing,
begin(), add(), send(), onReceive(), onSent(),
onNewPeer(), broadcast peers and ESP_NOW.write() all behave as before.
A first sketch
This is a small unicast example. It sends a short message once a second and prints whatever it receives. Set the peer MAC address to the address of the other board.
#include <Arduino.h>
#include "ESP32_NOW.h"
class Peer : public ESP_NOW_Peer {
public:
Peer(const uint8_t *mac) : ESP_NOW_Peer(mac, 6, WIFI_IF_STA, nullptr) {}
void onReceive(const uint8_t *data, size_t len, bool broadcast) {
Serial.write(data, len);
Serial.println();
}
using ESP_NOW_Peer::add;
using ESP_NOW_Peer::send;
};
uint8_t peerMac[6] = { 0xF0, 0xF5, 0xBD, 0x31, 0x9B, 0xB0 };
Peer peer(peerMac);
void setup() {
Serial.begin(115200);
ESP_NOW.setLink(6); // channel 6, uses the board variant's ESP32 UART
ESP_NOW.begin();
peer.add();
}
void loop() {
peer.send((const uint8_t *)"hello", 5);
for (uint32_t t = millis(); millis() - t < 1000;) {
ESP_NOW.poll();
delay(5);
}
}
More examples are installed with the library under File, Examples. They include the two canonical arduino-esp32 examples, Broadcast_Master and Broadcast_Slave, a Unicast_PingPong, a Discovery_PingPong that finds the other board by itself, and a RegressionSuite that runs a two-board self test of the whole API.
Board reset handling
setLink() opens the ESP32 UART. When the board variant also describes the
ESP32 reset pins, which all the iLabs Challenger WiFi and WiFi6 boards do, the
library performs a hardware reset of the co-processor into run mode and waits
for its +ENREADY message before it returns. Every time the host starts,
by power-on or by reset, the co-processor is therefore given a clean cold
start with no peers, keys or baud rate left over from an earlier session.
If the co-processor restarts on its own while running, for example after a
brownout, it sends +ENREADY again and the library records it. You can
react to this by registering a handler, or by polling wasReset() from
loop():
ESP_NOW.onReset([](void *) {
// The co-processor lost its peers and keys. The simplest recovery
// is a full restart of the host.
rp2040.reboot();
}, nullptr);
Discovery
ESP_NOW.discover() is an iLabs addition and has no arduino-esp32
counterpart. It sends a broadcast probe, and every board that runs the
interpreter answers in firmware without any help from its host. This is a
simple way to find the other boards on the current channel without hard-coding
MAC addresses:
ESP_NOW_Found found[8];
int n = ESP_NOW.discover(found, 8, 1000); // collect answers for 1 second
for (int i = 0; i < n; i++) {
// found[i].mac the responder STA MAC address
// found[i].rssi the signal strength this board measured
}
The call blocks for the collection window and returns the number of boards found, or -1 on error. It only covers the channel that is currently in use.
Notes and limitations
The callbacks run in the context of whoever calls
poll(), not in a background task. Keep them short, as you would on the ESP32.You may call library methods such as
peer.send()oradd()from inside a callback. The library defers such calls and runs them in order once the callback returns. For that reason a command issued from a callback returns before it has actually run, so do not issue a query from a callback and expect its answer straight away.getMaxDataLen()reports 250 bytes, which is the ESP-NOW version 1 limit. A unicast send above 248 bytes is fragmented by the firmware for you.Per-peer PHY rate, RSSI and statistics accessors, and ESP-NOW version 2 framing are not wrapped yet.