Built-in project · ESP32 DevKit V1

ESP-NOW button and LED on an ESP32 DevKit

Two ESP32s and no wire between them: the button on one sends on or off over ESP-NOW, the other lights its LED and prints each message, and the sender hears the ACK. The whole build, an ESP32 DevKit and 3 more parts, 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 Runs in your browser. Free, and no account needed.

The parts retail for about $18.25. On Mokxi they're free.

ESP-NOW button and LED · ESP32 DevKit V1live0.000 s 0.00x
Press Run, then press the button on the left board
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · ESP32 DevKit V1
// ESP-NOW sender: the button on GPIO 4 switches the LED on the other board.
//
// No wire and no router between the two ESP32s: ESP-NOW sends a frame straight
// to the receiver's MAC, and its radio answers with an ACK, which is what the
// send callback reports. In Mokxi every ESP board on the canvas shares the air.
//
// The receiver's MAC is written below. In this project the receiver's wifiMac
// setting is that address; on real boards, copy the line the receiver prints
// when it starts. Change one byte here and every send fails: nobody has that
// MAC, so no ACK comes back.

#include <WiFi.h>
#include <esp_now.h>

uint8_t receiverMac[] = {0x02, 0x4D, 0x4F, 0x00, 0x00, 0x02};

// The same struct on both boards: the receiver copies the bytes back into it.
typedef struct {
    bool on;
    uint32_t presses;
} Message;

const int kButton = 4; // to GND, with the pad's own pull-up
Message msg = {false, 0};
bool wasPressed = false;

// Arduino-ESP32 core 3.3's send callback. On cores 2.x to 3.2 the first
// argument was `const uint8_t *mac_addr`; Mokxi builds that one too.
void onSent(const wifi_tx_info_t *info, esp_now_send_status_t status)
{
    (void)info;
    Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery success" : "Delivery fail");
}

void setup()
{
    Serial.begin(115200);
    pinMode(kButton, INPUT_PULLUP);
    WiFi.mode(WIFI_STA);
    Serial.print("Sender MAC: ");
    Serial.println(WiFi.macAddress());

    if (esp_now_init() != ESP_OK) {
        Serial.println("Error initializing ESP-NOW");
        return;
    }
    esp_now_register_send_cb(onSent);

    esp_now_peer_info_t peer = {};
    memcpy(peer.peer_addr, receiverMac, 6);
    peer.channel = 0; // the radio's own channel
    peer.encrypt = false;
    if (esp_now_add_peer(&peer) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
    Serial.println("Press the button");
}

void loop()
{
    // Read every 20 ms: longer than the button bounces, so one press is one.
    bool pressed = digitalRead(kButton) == LOW;
    if (pressed && !wasPressed) {
        msg.on = !msg.on;
        msg.presses++;
        esp_err_t result = esp_now_send(receiverMac, (uint8_t *)&msg, sizeof(msg));
        Serial.print("Press ");
        Serial.print(msg.presses);
        Serial.print(": LED ");
        Serial.print(msg.on ? "on" : "off");
        Serial.println(result == ESP_OK ? ", sent" : ", not sent");
    }
    wasPressed = pressed;
    delay(20);
}
sketch.ino · ESP32 DevKit V1
// ESP-NOW receiver: lights the LED on GPIO 4 when the other board says so.
//
// It prints its own MAC first: that is the address the sender writes into its
// sketch. In this project the board's wifiMac setting fixes it at
// 02:4D:4F:00:00:02; empty, Mokxi gives each board a MAC of its own.
//
// The receive callback runs as soon as a frame arrives, as it does on a real
// ESP32, from the WiFi task rather than from loop().

#include <WiFi.h>
#include <esp_now.h>

typedef struct {
    bool on;
    uint32_t presses;
} Message;

const int kLed = 4; // through a resistor to GND

// Arduino-ESP32 core 3.x's receive callback; core 2.x's took the sender's MAC
// as `const uint8_t *mac` instead of `info`, and Mokxi builds that one too.
void onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len)
{
    if (len != sizeof(Message)) return;
    Message msg;
    memcpy(&msg, data, sizeof(msg));
    digitalWrite(kLed, msg.on ? HIGH : LOW);
    char from[18];
    snprintf(from, sizeof(from), "%02X:%02X:%02X:%02X:%02X:%02X", info->src_addr[0], info->src_addr[1],
             info->src_addr[2], info->src_addr[3], info->src_addr[4], info->src_addr[5]);
    Serial.print("From ");
    Serial.print(from);
    Serial.print(", ");
    Serial.print(len);
    Serial.print(" bytes: press ");
    Serial.print(msg.presses);
    Serial.print(", LED ");
    Serial.println(msg.on ? "on" : "off");
}

void setup()
{
    Serial.begin(115200);
    pinMode(kLed, OUTPUT);
    WiFi.mode(WIFI_STA);
    Serial.print("Receiver MAC: ");
    Serial.println(WiFi.macAddress());

    if (esp_now_init() != ESP_OK) {
        Serial.println("Error initializing ESP-NOW");
        return;
    }
    esp_now_register_recv_cb(onReceive);
    Serial.println("Waiting for the sender");
}

void loop()
{
    delay(1000);
}

Parts list

5 parts, plus the jumper wires. They retail for about $18.25; on Mokxi they're free.

How it is wired

5 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • ESP32 DevKit V1 (1) pin D4; Pushbutton pin 1a
  • ESP32 DevKit V1 (2) pin D4; Resistor, 220 Ω pin 1
  • Ground: Pushbutton pin 2a
  • Resistor, 220 Ω pin 2; LED, green pin A
  • Ground: LED, green pin C

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.