Built-in project · ESP8266 NodeMCU V1.0

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.

The circuit runs here once the page has loaded.

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 pinConnects to
Potentiometer pin 2ESP8266 NodeMCU V1.0 (1) pin A0
Potentiometer pin 1ESP8266 NodeMCU V1.0 (1) pin GND
Potentiometer pin 3ESP8266 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.

sketch.ino · ESP8266 NodeMCU V1.0
// 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));
    }
}
sketch.ino · ESP8266 NodeMCU V1.0
// 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 that receiverMac matches the receiver's MAC, the one it prints at start-up.
  • The receiver prints nothing This project
    Both sketches must use the same Message struct. 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.