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.
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
lenwon'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.channelor callesp_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 = trueand configure keys withesp_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.