← All projects
Wireless Intermediate

ESP-NOW: A Wireless Remote Between Two ESP32s — No Router Needed

ESP-NOW lets boards exchange short messages directly with just a few milliseconds of latency. We build a button remote and an LED receiver.

Difficulty
Intermediate
Time
1–2 h
Category
Wireless

What is ESP-NOW

ESP-NOW is Espressif's own protocol on top of the 2.4 GHz radio. Boards exchange packets of up to 250 bytes directly by MAC address, with no access point and no connection setup:

  • latency of a few milliseconds;
  • range of tens of metres indoors, 100+ m in open space;
  • perfect for remotes, battery sensors and "many sensors → one gateway" setups.

Parts

  • 2 × ESP32 DevKit
  • LED + 330 Ω resistor (or the on-board LED on GPIO 2)
  • A button — the on-board BOOT button (GPIO 0) works fine

Step 1. Get the receiver's MAC

Flash the code below to the receiver board — it prints its MAC address to the Serial Monitor.

Receiver

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

typedef struct {
  uint8_t pressed;
  float chipTemp;
} Message;

const int LED_PIN = 2;

// Callback signature for arduino-esp32 core 3.x
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(LED_PIN, msg.pressed ? HIGH : LOW);

  const uint8_t *m = info->src_addr;
  Serial.printf("From %02X:%02X:%02X:%02X:%02X:%02X  pressed=%d  chip=%.1f°C\n",
                m[0], m[1], m[2], m[3], m[4], m[5], msg.pressed, msg.chipTemp);
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  WiFi.mode(WIFI_STA);
  Serial.print("Receiver MAC: ");
  Serial.println(WiFi.macAddress());

  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW init failed");
    return;
  }
  esp_now_register_recv_cb(onReceive);
}

void loop() {}

Note the address, something like 24:6F:28:AA:BB:CC.

Step 2. Sender

Put the receiver's MAC into receiverMac:

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

uint8_t receiverMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};  // ← your MAC

typedef struct {
  uint8_t pressed;
  float chipTemp;
} Message;

const int BUTTON_PIN = 0;  // BOOT button
int lastState = HIGH;

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW init failed");
    return;
  }

  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, receiverMac, 6);
  peer.channel = 0;      // current channel
  peer.encrypt = false;
  if (esp_now_add_peer(&peer) != ESP_OK) {
    Serial.println("Failed to add peer");
  }
}

void loop() {
  int state = digitalRead(BUTTON_PIN);
  if (state != lastState) {
    lastState = state;
    Message msg = { (uint8_t)(state == LOW), temperatureRead() };
    esp_err_t res = esp_now_send(receiverMac, (uint8_t *)&msg, sizeof(msg));
    Serial.println(res == ESP_OK ? "sent" : "send error");
    delay(30);  // simple debounce
  }
}

Testing

Press BOOT on the sender — the receiver's LED lights up and its Serial Monitor shows the sender's MAC and chip temperature.

Things to know

  • The message struct must be identical on both boards, otherwise len won't match.
  • Wi-Fi channel. If the receiver is also connected to a router, ESP-NOW runs on the router's channel. The sender must use the same one (set peer.channel or call esp_wifi_set_channel()).
  • Core 2.x uses a different callback signature: void onReceive(const uint8_t *mac, const uint8_t *data, int len).
  • For encryption set peer.encrypt = true and configure keys with esp_now_set_pmk() / peer.lmk.

Where to take it

  • Battery sensor: wake up → send via ESP-NOW (≈50 ms) → sleep. Far more efficient than Wi-Fi.
  • An "ESP-NOW → MQTT" gateway: one board collects data from dozens of sensors and forwards it to Home Assistant.