ESP-NOW on two NodeMCUs
Two ESP8266 boards talking ESP-NOW with the ESP8266 core’s espnow.h: one sends a count and a knob reading every two seconds, the other prints it and blinks. The whole build, a NodeMCU ESP8266 and 1 more part, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 25 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $10.50. On Mokxi they're free.
What you'll learn
- Sending data between boards with ESP-NOW
- MAC addresses as device addresses
- Packing values into a struct to send
- Callbacks for sending and receiving
- Controller and slave roles
- Difficulty: Intermediate
- Time: About 25 minutes
- Board: ESP8266 NodeMCU V1.0
- Parts: 3
How it works
Two NodeMCU boards talk directly over ESP-NOW, with no router. The sender is wired to a potentiometer on A0. Every two seconds it sends a small message: a text, a running count and the knob reading from 0 to 1023.
The sender needs to know where to aim, so the receiver's MAC address is written into its sketch. When the receiver's radio acknowledges a frame, the sender's callback prints a delivery success.
The receiver prints its own MAC at start-up, then prints each message it gets, with the sender's address. The built-in LED blinks once per message.
Parts list
3 parts, plus the jumper wires. They retail for about $10.50; on Mokxi they're free.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Potentiometer pin 2 | ESP8266 NodeMCU V1.0 (1) pin A0 |
| Potentiometer pin 1 | ESP8266 NodeMCU V1.0 (1) pin GND |
| Potentiometer pin 3 | ESP8266 NodeMCU V1.0 (1) pin 3V3 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// ESP-NOW on the ESP8266: a count and a knob reading every two seconds to the other board.
//
// The ESP8266 core's espnow.h, as the NodeMCU tutorials use it: a role, a
// peer added with its channel, and a send callback that gets 0 when the
// receiver's radio acknowledged the frame. The receiver's MAC is written
// below; in this project its wifiMac setting is that address.
#include <ESP8266WiFi.h>
#include <espnow.h>
uint8_t receiverMac[] = {0x02, 0x4D, 0x4F, 0x00, 0x82, 0x66};
typedef struct {
char text[32];
int count;
int dial; // the knob on A0, 0 to 1023
} Message;
Message msg;
unsigned long lastSend = 0;
void onSent(uint8_t *mac, uint8_t status)
{
(void)mac;
Serial.print("Last packet send status: ");
Serial.println(status == 0 ? "Delivery success" : "Delivery fail");
}
void setup()
{
Serial.begin(115200);
WiFi.mode(WIFI_STA);
Serial.print("Sender MAC: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != 0) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
esp_now_register_send_cb(onSent);
esp_now_add_peer(receiverMac, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);
}
void loop()
{
if (millis() - lastSend > 2000) {
lastSend = millis();
strcpy(msg.text, "hello from the NodeMCU");
msg.count++;
msg.dial = analogRead(A0);
esp_now_send(receiverMac, (uint8_t *)&msg, sizeof(msg));
}
}
// ESP-NOW on the ESP8266: prints what the other board sends.
//
// It prints its own MAC first, the address the sender writes into its sketch
// (in this project the board's wifiMac setting, 02:4D:4F:00:82:66). The
// on-board LED on D4 is active low and blinks once for each message.
#include <ESP8266WiFi.h>
#include <espnow.h>
typedef struct {
char text[32];
int count;
int dial;
} Message;
volatile bool got = false;
void onReceive(uint8_t *mac, uint8_t *data, uint8_t len)
{
Message msg;
if (len != sizeof(msg)) return;
memcpy(&msg, data, sizeof(msg));
Serial.printf("From %02X:%02X:%02X:%02X:%02X:%02X, %u bytes: %s, count %d, dial %d\n", mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5], len, msg.text, msg.count, msg.dial);
got = true;
}
void setup()
{
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
WiFi.mode(WIFI_STA);
Serial.print("Receiver MAC: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != 0) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
esp_now_register_recv_cb(onReceive);
}
void loop()
{
if (got) {
got = false;
digitalWrite(LED_BUILTIN, LOW);
delay(100);
digitalWrite(LED_BUILTIN, HIGH);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
On the sender: puts the radio in station mode, prints its MAC and starts ESP-NOW. It sets the controller role, registers the send callback onSent, and adds the receiver as a peer on channel 1. On the receiver: the same start, but with the slave role and onReceive registered.
void setup()
{
Serial.begin(115200);
WiFi.mode(WIFI_STA);
Serial.print("Sender MAC: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != 0) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
esp_now_register_send_cb(onSent);
esp_now_add_peer(receiverMac, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);
}loop sketch.ino
On the sender, every 2000 ms it fills the message with the text, adds one to count, reads the knob into dial and sends the whole struct. On the receiver the loop only watches a flag set by the callback, and blinks the LED for 100 ms.
void loop()
{
if (millis() - lastSend > 2000) {
lastSend = millis();
strcpy(msg.text, "hello from the NodeMCU");
msg.count++;
msg.dial = analogRead(A0);
esp_now_send(receiverMac, (uint8_t *)&msg, sizeof(msg));
}
}onSent sketch.ino
Runs after each send. It gets a status of 0 when the receiver's radio acknowledged the frame, and prints "Delivery success". Anything else prints "Delivery fail".
void onSent(uint8_t *mac, uint8_t status)
{
(void)mac;
Serial.print("Last packet send status: ");
Serial.println(status == 0 ? "Delivery success" : "Delivery fail");
}onReceive sketch.ino
Runs when a frame arrives. It ignores any frame whose length is not the size of Message, copies the bytes into a struct and prints the sender's MAC, text, count and dial. It sets a flag so the main loop can blink the LED.
void onReceive(uint8_t *mac, uint8_t *data, uint8_t len)
{
Message msg;
if (len != sizeof(msg)) return;
memcpy(&msg, data, sizeof(msg));
Serial.printf("From %02X:%02X:%02X:%02X:%02X:%02X, %u bytes: %s, count %d, dial %d\n", mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5], len, msg.text, msg.count, msg.dial);
got = true;
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The sender always says "Delivery fail" This project
Check thatreceiverMacmatches the receiver's MAC, the one it prints at start-up. - The receiver prints nothing This project
Both sketches must use the sameMessagestruct. A different size makes the receiver drop the frame. - The reading jumps around or only covers part of the range. Potentiometer
Wire the two outer legs to 5 V and ground and the middle leg (the wiper) to an analog pin. Using the wrong two legs gives a dead or partial range. - Turning the knob does nothing, or the value is stuck at 0 or 1023. Potentiometer
Check that the wiper goes to an analog input (A0 to A5 on an Uno), not a digital pin. Make sure both ends are connected, since a floating end gives noise. - The D numbers on the silkscreen do not match GPIO numbers, so the wrong pin is driven. D0 is GPIO16, D1 is GPIO5, D2 is GPIO4, D3 is GPIO0, D4 is GPIO2. ESP8266 NodeMCU V1.0
Use the D names from the board package (D1, D2) or look up the real GPIO. The built-in LEDs are active low, so LOW turns them on. - The board will not boot or flash. GPIO0, GPIO2 and GPIO15 set the boot mode, so pulling them the wrong way at reset stops it. ESP8266 NodeMCU V1.0
Keep D3 (GPIO0) and D4 (GPIO2) high and D8 (GPIO15) low at reset. Avoid hanging switches or loads on those pins at startup.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.