ESP-NOW con ESP32: guía de comunicación inalámbrica sin router
ESP-NOW con ESP32: obtén la dirección MAC, envía un struct de hasta 250 bytes en una o dos direcciones, usa 20 pares, alcance, core 3.x y el canal Wi-Fi.
ESP-NOW es un protocolo inalámbrico desarrollado por Espressif que permite a las placas ESP32 y ESP8266 enviarse datos directamente sin router, punto de acceso ni conexión a internet. Las placas usan su radio Wi-Fi, pero nunca se unen a una red: cada dispositivo encuentra al otro por su dirección MAC y entrega paquetes pequeños de hasta 250 bytes en pocos milisegundos. Como no hay que establecer conexión, obtener una dirección IP ni escribir contraseñas, al pulsar un botón en una placa puedes activar al instante un relé en otra, y un sensor a batería puede despertarse, enviar su lectura y volver a dormir enseguida.
En esta guía vas a configurar ESP-NOW desde cero: primero leerás la dirección MAC de tus placas y después, con ejemplos en una y dos direcciones, enviarás valores distintos como temperatura, un contador y texto en un único paquete usando un struct. Luego conocerás el límite de 20 pares, los mensajes de difusión (broadcast), el alcance y la velocidad, las funciones que cambiaron con el core 3.x de Arduino para ESP32 y la coincidencia de canal, la única regla para usar ESP-NOW y Wi-Fi a la vez. En la última parte enviarás las lecturas de un sensor remoto a batería en sueño profundo a una pasarela conectada al Wi-Fi y las verás desde el móvil. Si es tu primer proyecto con ESP32, completa antes la instalación del Arduino IDE de ¿Qué es el ESP32?.

¿Qué es ESP-NOW y cómo funciona?
Con el Wi-Fi normal, el ESP32 primero se conecta al router, comprueba la contraseña, obtiene una dirección IP por DHCP y solo entonces puede enviar datos; esto suele tardar entre 1 y 3 segundos. ESP-NOW se salta todos estos pasos. Mete tus datos en un paquete Wi-Fi corto llamado trama de acción específica del fabricante (vendor-specific action frame) del estándar IEEE 802.11 y lo envía directamente a la dirección MAC de la otra placa. La placa que lo recibe responde con una trama de confirmación (ACK) a nivel de hardware, y una función callback en el lado emisor te dice si el paquete se entregó. Si no llega el ACK, la capa Wi-Fi reintenta el envío automáticamente varias veces. En resumen, ESP-NOW funciona como un buzón invisible entre dos placas: escribes la dirección, echas el paquete y te enteras cuando el otro lado lo ha recibido.
| Característica | Valor |
|---|---|
| Desarrollado por | Espressif Systems |
| Frecuencia | 2,4 GHz (radio Wi-Fi, canales 1–13) |
| Datos por paquete | 250 bytes (ESP-NOW v1); 1470 bytes con v2 en ESP-IDF 5.4 y posteriores |
| Pares registrados | Hasta 20; hasta 7 cifrados por defecto |
| Velocidad de datos por defecto | 1 Mbps |
| Confirmación de entrega | Sí en unicast (ACK), no en difusión |
| Cifrado | Opcional, CCMP (AES-128); claves PMK y LMK de 16 bytes cada una |
| Router / punto de acceso | No hace falta |
| Chips compatibles | Chips ESP32 con Wi-Fi (ESP32, S2, S3, C2, C3, C6…) y ESP8266 |
Las verdaderas ventajas de ESP-NOW son la respuesta rápida y el bajo consumo. Cuando pulsas un botón, el LED del otro lado se enciende en pocos milisegundos; hacer lo mismo por Wi-Fi exige conectarse primero y luego enviar una petición a un servidor. En un sensor a batería la diferencia es aún mayor: al conectarse al Wi-Fi la radio consume más de 100 mA durante segundos, mientras que con ESP-NOW la placa se despierta, envía su lectura y vuelve a dormir en unos pocos cientos de milisegundos en total. A cambio, ESP-NOW no llega a internet; si quieres llevar tus datos a internet o al móvil, necesitas en algún punto de la red un ESP32 conectado al Wi-Fi que haga de pasarela (gateway). Montaremos ese esquema más adelante en esta guía.
ESP-NOW, Wi-Fi, Bluetooth o nRF24L01: ¿cuál elegir?
| Método | Módulo extra | Router | Tamaño de paquete | Alcance típico (campo abierto) | Ideal para |
|---|---|---|---|---|---|
| ESP-NOW | No hace falta | No hace falta | 250 bytes | 100–200 m, más en modo LR | Órdenes y datos de sensores entre placas |
| Wi-Fi (HTTP, MQTT) | No hace falta | Necesario | Prácticamente ilimitado | La cobertura del router | Internet, páginas web, nube |
| Bluetooth (BLE) | No hace falta | No hace falta | 20 bytes por defecto | 10–20 m en interiores | Conectar con el móvil |
| nRF24L01 | Necesario (SPI) | No hace falta | 32 bytes | Decenas de metros, mucho más con la versión PA+LNA | Placas sin radio como Arduino Uno y Nano |
| LoRa (SX1278) | Necesario (SPI) | No hace falta | Hasta 255 bytes | Varios kilómetros | Distancias muy largas, datos esporádicos |
Si tienes que hablar con el móvil, Bluetooth es la opción correcta; si necesitas internet, usa Wi-Fi. Pero si tienes varios ESP32 que solo deben comunicarse entre sí, ESP-NOW suele ser el camino más sencillo: sin módulos extra, sin cables, sin router y con pocas líneas de código. Para placas sin radio como el Arduino Uno encaja mejor el nRF24L01, y para kilómetros de alcance, los módulos LoRa. Para comparar todas las opciones, consulta opciones de comunicación inalámbrica para Arduino, y para conectar con el móvil tienes nuestra guía de Bluetooth en el ESP32.
¿Qué placas son compatibles con ESP-NOW?
| Placa / chip | ESP-NOW | Nota |
|---|---|---|
| ESP32 (WROOM-32, DevKit) | Sí | También admite el modo de largo alcance (LR) |
| ESP32-S2, S3, C3, C6 | Sí | El mismo código de Arduino, solo cambia la placa elegida |
| ESP32-C2 (ESP8684) | Sí | Sin modo LR |
| ESP32-H2 | No | No tiene radio Wi-Fi, así que no hay ESP-NOW |
| ESP8266 (NodeMCU, Wemos D1 Mini) | Sí | Biblioteca distinta (espnow.h), solo v1 y 250 bytes |
| Raspberry Pi Pico W / Pico 2 W | No | Su chip Wi-Fi (CYW43439) no ejecuta ESP-NOW |
| Arduino Uno, Nano, Mega | No | Se puede conectar a un ESP32 por puerto serie (UART) |
ESP-NOW también funciona entre chips ESP distintos: un sensor ESP32-C3 puede enviar paquetes a una pasarela ESP32 clásica o a una placa ESP8266. Las únicas condiciones son que ambos lados estén en el mismo canal Wi-Fi y que la estructura de datos esté definida exactamente igual en los dos. Los ejemplos siguientes usan dos placas ESP32 DevKit; puedes ver qué pines de tu placa son seguros en la herramienta Pinout.
Paso 1: obtener la dirección MAC del ESP32
En ESP-NOW cada placa encuentra a la otra por su dirección MAC de 6 bytes; esta dirección se graba en cada ESP32 en fábrica y no cambia. Sube el siguiente sketch a la placa que usarás como receptora y abre el Monitor Serie a 115200 baudios. El código imprime la dirección en formato legible y también como una línea de array que puedes pegar directamente en el código de la otra placa. En los ejemplos con dos placas, hazlo con ambas y apunta las direcciones.
1#include <WiFi.h>
2#include <esp_wifi.h>
3
4void setup() {
5 Serial.begin(115200);
6 delay(1000);
7 WiFi.mode(WIFI_STA); // ESP-NOW istasyon arayüzünü kullanır / ESP-NOW uses the station interface
8
9 uint8_t mac[6];
10 if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) {
11 Serial.printf("STA MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
12 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
13 // Diğer karta kopyalanacak satır / line to paste into the other board's code
14 Serial.printf("uint8_t karsiMac[] = {0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X};\n",
15 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
16 } else {
17 Serial.println("MAC okunamadi / could not read MAC");
18 }
19}
20
21void loop() {
22}Enviar datos con un struct y el límite de 250 bytes
ESP-NOW solo envía un array de bytes y no sabe qué contiene. La forma más limpia de enviar varios valores a la vez es agruparlos en un struct y mandar sus bytes tal como están en memoria. El receptor define el mismo struct y copia sobre él los bytes recibidos con memcpy. Para que funcione, el struct debe tener el mismo orden, los mismos tipos y el mismo tamaño en los dos lados. El compilador puede añadir bytes de relleno invisibles entre los campos para acceder más rápido: el struct Paket de los ejemplos ocupa normalmente 36 bytes, pero baja a exactamente 30 bytes al añadir __attribute__((packed)). Usar packed garantiza que cada byte esté en el mismo sitio entre un ESP32 y un ESP8266, o entre Arduino y MicroPython.
| Tipo | Tamaño | Nota |
|---|---|---|
| bool, uint8_t, int8_t, char | 1 byte | Indicador, número pequeño, un carácter |
| int16_t, uint16_t | 2 bytes | −32768…32767 o 0…65535 |
| int, int32_t, uint32_t | 4 bytes | int ocupa 4 bytes en ESP32 y ESP8266, pero 2 bytes en Arduino Uno |
| float | 4 bytes | Unos 7 dígitos de precisión |
| double | 8 bytes | Usa float salvo que de verdad lo necesites |
| char mesaj[16] | 16 bytes | Texto de longitud fija; debe terminar en '\0' |
| String, char*, puntero | No se puede enviar | Solo viaja una dirección de memoria, que no significa nada al otro lado |
En un paquete caben como máximo 250 bytes. Este límite viene de la primera versión de ESP-NOW (v1) y se aplica a todas las placas, incluido el ESP8266. El core 3.2 de Arduino y posteriores, basados en ESP-IDF 5.4, pueden enviar paquetes de hasta 1470 bytes con ESP-NOW v2; sin embargo, una placa con v1 (un ESP8266 o un ESP32 con un core antiguo) recorta los paquetes de más de 250 bytes o los descarta por completo. Por eso, en una red mixta lo más seguro es no pasar de 250 bytes. La línea static_assert del sketch emisor detiene la compilación con un mensaje claro si el struct supera los 250 bytes. Si necesitas enviar datos más grandes, divídelos en trozos numerados y únelos en orden en el receptor; para valores de sensores, estados de botones u órdenes, unas pocas decenas de bytes sobran.
ESP-NOW en una dirección: el sketch emisor
- Asegúrate de que las dos placas ESP32 están seleccionadas como ESP32 Dev Module en el Arduino IDE. La biblioteca ESP-NOW viene incluida en el paquete de la placa, no hay que instalar nada más.
- Escribe la dirección MAC que leíste en la placa receptora en el array aliciMac del código siguiente.
- Conecta un pulsador entre GPIO 4 y GND en el emisor; como usamos INPUT_PULLUP, no hace falta resistencia adicional. Tienes los detalles en cómo conectar un pulsador a Arduino.
- Sube el sketch al emisor y abre el Monitor Serie a 115200 baudios. Mientras el receptor aún no está funcionando, es normal ver "Teslim edilemedi / failed" cada dos segundos.
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Alıcının STA MAC adresi (mac_oku çıktısı) / receiver's STA MAC address (from the MAC sketch)
6uint8_t aliciMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};
7
8const int BUTON_PIN = 4; // buton GPIO 4 ile GND arasında / button between GPIO 4 and GND
9
10// İki kartta da birebir aynı olmalı / must be identical on both boards
11typedef struct __attribute__((packed)) {
12 uint8_t id; // gönderen kart numarası / sender board number
13 uint32_t sayac; // paket sayacı / packet counter
14 float sicaklik; // °C
15 float nem; // % bağıl nem / % relative humidity
16 bool buton; // buton basılı mı / is the button pressed
17 char mesaj[16]; // kısa metin, String değil / short text, not String
18} Paket;
19static_assert(sizeof(Paket) <= 250, "Paket 250 bayti asamaz / packet must not exceed 250 bytes");
20
21Paket paket;
22
23// Teslim sonucu: alıcı paketi aldıysa SUCCESS / delivery result: SUCCESS if the receiver got it
24#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
25void gonderimSonucu(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) { // çekirdek 3.3+ / core 3.3+
26#else
27void gonderimSonucu(const uint8_t *mac, esp_now_send_status_t durum) { // çekirdek 2.x-3.2 / core 2.x-3.2
28#endif
29 Serial.println(durum == ESP_NOW_SEND_SUCCESS ? "Teslim edildi / delivered" : "Teslim edilemedi / failed");
30}
31
32void setup() {
33 Serial.begin(115200);
34 pinMode(BUTON_PIN, INPUT_PULLUP);
35 WiFi.mode(WIFI_STA); // önce Wi-Fi, sonra ESP-NOW / Wi-Fi first, then ESP-NOW
36
37 if (esp_now_init() != ESP_OK) {
38 Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
39 return;
40 }
41 esp_now_register_send_cb(gonderimSonucu);
42
43 esp_now_peer_info_t es = {}; // tüm alanları sıfırla / zero every field
44 memcpy(es.peer_addr, aliciMac, 6);
45 es.channel = 0; // 0 = şu anki kanal / 0 = current channel
46 es.ifidx = WIFI_IF_STA;
47 es.encrypt = false;
48 if (esp_now_add_peer(&es) != ESP_OK) {
49 Serial.println("Es eklenemedi / failed to add peer");
50 return;
51 }
52 paket.id = 1;
53}
54
55void loop() {
56 paket.sayac++;
57 paket.sicaklik = 20.0 + random(0, 100) / 10.0; // deneme değeri / test value
58 paket.nem = 40.0 + random(0, 200) / 10.0;
59 paket.buton = (digitalRead(BUTON_PIN) == LOW);
60 snprintf(paket.mesaj, sizeof(paket.mesaj), "Selam %lu", (unsigned long)paket.sayac);
61
62 esp_err_t sonuc = esp_now_send(aliciMac, (uint8_t *)&paket, sizeof(paket));
63 if (sonuc != ESP_OK) {
64 Serial.printf("Gonderme hatasi / send error: %s\n", esp_err_to_name(sonuc));
65 }
66 delay(2000);
67}El sketch arranca primero el Wi-Fi en modo WIFI_STA; ESP-NOW no funciona sin la radio Wi-Fi, así que esp_now_init() debe ir siempre después de la línea WiFi.mode(). Después, el receptor se añade a la lista como par (peer). Es importante poner a cero la estructura esp_now_peer_info_t con = {}; los valores aleatorios que quedan en los campos sin borrar provocan el error "Peer interface is invalid". channel = 0 significa "usa el canal actual de la placa". esp_now_send() pone el paquete en una cola y vuelve enseguida; que devuelva ESP_OK solo indica que el paquete se aceptó para enviarlo. El resultado real llega unos milisegundos después a la función gonderimSonucu(): ESP_NOW_SEND_SUCCESS significa que el receptor confirmó el paquete a nivel de hardware. El bloque #if del principio elige por sí solo la forma correcta del callback de envío, que depende de la versión del core; lo explicamos en la sección del core 3.x.
Sketch receptor de ESP-NOW
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Vericidekiyle birebir aynı yapı / exactly the same struct as the sender
6typedef struct __attribute__((packed)) {
7 uint8_t id;
8 uint32_t sayac;
9 float sicaklik;
10 float nem;
11 bool buton;
12 char mesaj[16];
13} Paket;
14
15Paket gelen;
16volatile bool yeniPaket = false;
17
18// Wi-Fi görevinde çalışır: kısa tut / runs in the Wi-Fi task: keep it short
19#if ESP_IDF_VERSION_MAJOR >= 5
20void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) { // çekirdek 3.x / core 3.x
21#else
22void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) { // çekirdek 2.x / core 2.x
23#endif
24 if (uzunluk != sizeof(Paket)) return; // farklı boyut: başka bir paket / wrong size: some other packet
25 memcpy(&gelen, veri, sizeof(Paket)); // veriyi hemen kopyala / copy the data right away
26 yeniPaket = true;
27}
28
29void setup() {
30 Serial.begin(115200);
31 WiFi.mode(WIFI_STA);
32
33 if (esp_now_init() != ESP_OK) {
34 Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
35 return;
36 }
37 esp_now_register_recv_cb(veriGeldi);
38 Serial.println("Alici hazir / receiver ready");
39}
40
41void loop() {
42 if (yeniPaket) {
43 yeniPaket = false;
44 Paket p = gelen; // kopya üzerinde çalış / work on a copy
45 p.mesaj[sizeof(p.mesaj) - 1] = '\0'; // metin her zaman sonlansın / always terminate the text
46 Serial.printf("id=%u sayac=%lu sicaklik=%.1f nem=%.1f buton=%d mesaj=%s\n",
47 (unsigned)p.id, (unsigned long)p.sayac, p.sicaklik, p.nem, (int)p.buton, p.mesaj);
48 }
49}En el receptor, los paquetes llegan a la función veriGeldi(). Esta función no se ejecuta en tu loop(), sino en la tarea de alta prioridad del Wi-Fi, así que no debes usar dentro Serial.print, delay ni cálculos largos. Lo correcto es comprobar que el tamaño del paquete coincide con el struct esperado, copiar los datos a tu propia variable con memcpy y avisar a loop() mediante un indicador volatile. En el core 3.x, el parámetro bilgi indica además quién envió el paquete (bilgi->src_addr) y la intensidad de la señal (bilgi->rx_ctrl->rssi, en dBm); esta información solo es válida dentro de la función, así que cópiala si la necesitas después. El receptor no necesita añadir al emisor como par: los paquetes unicast sin cifrar y las difusiones se aceptan también de placas que no están en la lista.
Comunicación bidireccional: dos ESP32 que se controlan entre sí
En la comunicación bidireccional cada placa es emisora y receptora a la vez. Subirás el siguiente sketch a las dos placas; la única diferencia es que en cada una escribes en el array karsiMac la dirección de la otra. Al pulsar el botón de una placa, el LED de la otra se enciende o se apaga, y la placa que cambió su LED lo confirma con un mensaje de respuesta. El cableado es igual en las dos placas:
| Componente | Pin del ESP32 | Nota |
|---|---|---|
| Pulsador | GPIO 4 y GND | Usa el pull-up interno, no necesita resistencia extra |
| LED | GPIO 2 | LED azul integrado en la mayoría de DevKit; para un LED externo, resistencia de 220 Ω |
| Alimentación | USB | Las placas pueden alimentarse desde puertos USB o cargadores distintos |
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_idf_version.h>
4
5// Bu kodu iki karta da yükle; yalnız karşı kartın MAC adresini değiştir
6// Upload to both boards; change only the other board's MAC address
7uint8_t karsiMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};
8
9const int BUTON_PIN = 4; // buton GPIO 4 ile GND arasında / button between GPIO 4 and GND
10const int LED_PIN = 2; // dahili LED / onboard LED
11
12typedef struct __attribute__((packed)) {
13 uint8_t tur; // 1 = komut, 2 = onay / 1 = command, 2 = acknowledgement
14 bool ledAcik; // istenen ya da uygulanan durum / requested or applied state
15} Mesaj;
16
17volatile bool komutGeldi = false;
18volatile bool onayGeldi = false;
19volatile bool gelenLed = false;
20bool karsiLed = false; // karşı karttaki LED için son isteğimiz / our last request for the other LED
21int sonButon = HIGH;
22
23#if ESP_IDF_VERSION_MAJOR >= 5
24void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) { // çekirdek 3.x / core 3.x
25#else
26void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) { // çekirdek 2.x / core 2.x
27#endif
28 if (uzunluk != sizeof(Mesaj)) return;
29 Mesaj m;
30 memcpy(&m, veri, sizeof(m));
31 if (m.tur == 1) {
32 gelenLed = m.ledAcik;
33 komutGeldi = true;
34 } else if (m.tur == 2) {
35 onayGeldi = true;
36 }
37}
38
39void gonder(uint8_t tur, bool ledAcik) {
40 Mesaj m = {tur, ledAcik};
41 esp_now_send(karsiMac, (uint8_t *)&m, sizeof(m));
42}
43
44void setup() {
45 Serial.begin(115200);
46 pinMode(BUTON_PIN, INPUT_PULLUP);
47 pinMode(LED_PIN, OUTPUT);
48 WiFi.mode(WIFI_STA);
49
50 if (esp_now_init() != ESP_OK) {
51 Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
52 return;
53 }
54 esp_now_register_recv_cb(veriGeldi);
55
56 esp_now_peer_info_t es = {};
57 memcpy(es.peer_addr, karsiMac, 6);
58 es.ifidx = WIFI_IF_STA; // kanal 0, şifresiz / channel 0, unencrypted
59 esp_now_add_peer(&es);
60}
61
62void loop() {
63 // 1) Butona basılınca karşı kartın LED'ini değiştir / on press, toggle the other board's LED
64 int buton = digitalRead(BUTON_PIN);
65 if (buton != sonButon) {
66 delay(30); // kontak sıçramasını bekle / wait out contact bounce
67 buton = digitalRead(BUTON_PIN);
68 if (sonButon == HIGH && buton == LOW) {
69 karsiLed = !karsiLed;
70 gonder(1, karsiLed);
71 Serial.println(karsiLed ? "Komut: AC / command: ON" : "Komut: KAPAT / command: OFF");
72 }
73 sonButon = buton;
74 }
75
76 // 2) Gelen komutu uygula ve onay gönder / apply an incoming command and reply
77 if (komutGeldi) {
78 komutGeldi = false;
79 digitalWrite(LED_PIN, gelenLed ? HIGH : LOW);
80 gonder(2, gelenLed);
81 }
82
83 // 3) Karşı karttan gelen onay / acknowledgement from the other board
84 if (onayGeldi) {
85 onayGeldi = false;
86 Serial.println("Karsi kart uyguladi / the other board applied it");
87 }
88}El campo tur del struct de mensaje indica qué es el paquete: 1 es una orden y 2 una confirmación. La placa en la que se pulsa el botón envía el estado que quiere para el LED de la otra; la placa que recibe la orden ajusta su LED y devuelve el mismo estado con el tipo 2. Así el emisor recibe tanto el ACK de hardware como la confirmación de que "el LED cambió de verdad". La orden recibida se pasa de nuevo a loop() con indicadores volatile, y digitalWrite y la respuesta se hacen allí. Es más seguro que llamar a esp_now_send() dentro del callback. La espera de 30 ms en el pulsador filtra los rebotes de los contactos. Si conectas un relé en lugar del LED, el mismo código se convierte en un interruptor de luz inalámbrico; para el cableado del relé consulta ¿Qué es un módulo relé?.
Varios dispositivos: el límite de 20 pares y la difusión (broadcast)
ESP-NOW admite las tres configuraciones: un emisor a muchos receptores (un mando a cinco placas con relé), muchos emisores a un receptor (cinco sensores a una sola pasarela) y redes en las que todos hablan con todos. La lista de pares de una placa admite como máximo 20 entradas, y por defecto solo 7 pueden estar cifradas. Este límite solo afecta a las placas a las que envías. Una pasarela no tiene que añadir a los sensores que le envían paquetes sin cifrar, así que puede escuchar a más de 20 sensores. Si la lista está llena, esp_now_add_peer() devuelve ESP_ERR_ESPNOW_FULL; entonces puedes borrar con esp_now_del_peer() los pares que ya no necesitas y añadir otros nuevos. El siguiente sketch añade tres receptores y la dirección de difusión a la lista, y luego envía tanto uno por uno como por difusión:
1#include <WiFi.h>
2#include <esp_now.h>
3
4// Üç alıcının STA MAC adresleri / STA MAC addresses of three receivers
5uint8_t alicilar[3][6] = {
6 {0x24, 0x6F, 0x28, 0x11, 0x11, 0x11},
7 {0x24, 0x6F, 0x28, 0x22, 0x22, 0x22},
8 {0x24, 0x6F, 0x28, 0x33, 0x33, 0x33}
9};
10uint8_t yayinMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // yayın adresi / broadcast address
11
12typedef struct __attribute__((packed)) {
13 uint8_t hedef; // 0 = herkes, 1-3 = tek alıcı / 0 = everyone, 1-3 = one receiver
14 uint32_t sayac;
15} Komut;
16
17Komut komut;
18
19bool esEkle(const uint8_t *mac) {
20 esp_now_peer_info_t es = {};
21 memcpy(es.peer_addr, mac, 6);
22 es.ifidx = WIFI_IF_STA;
23 return esp_now_add_peer(&es) == ESP_OK;
24}
25
26void setup() {
27 Serial.begin(115200);
28 WiFi.mode(WIFI_STA);
29 if (esp_now_init() != ESP_OK) {
30 Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
31 return;
32 }
33 for (int i = 0; i < 3; i++) esEkle(alicilar[i]);
34 esEkle(yayinMac); // yayın da eş olarak eklenmeli / broadcast must be added as a peer too
35
36 esp_now_peer_num_t sayi;
37 esp_now_get_peer_num(&sayi);
38 Serial.printf("Kayitli es / registered peers: %d / 20\n", sayi.total_num);
39}
40
41void loop() {
42 komut.sayac++;
43
44 // 1) Tek tek: her alıcıya ayrı paket ve ayrı teslim onayı / one by one: separate packet and ACK each
45 for (int i = 0; i < 3; i++) {
46 komut.hedef = i + 1;
47 esp_now_send(alicilar[i], (uint8_t *)&komut, sizeof(komut));
48 delay(10);
49 }
50 delay(2000);
51
52 // 2) Yayın: tek paket, aynı kanaldaki herkes alır, onay yok / broadcast: one packet, everyone on the channel, no ACK
53 komut.hedef = 0;
54 esp_now_send(yayinMac, (uint8_t *)&komut, sizeof(komut));
55 delay(2000);
56}La dirección de difusión FF:FF:FF:FF:FF:FF también debe añadirse como par antes de enviar. Un solo paquete de difusión llega a todos los receptores ESP-NOW del mismo canal; no necesitas conocer sus direcciones MAC, por eso es el método más fácil de montar. El precio es que la difusión no tiene confirmación de entrega, el resultado del envío siempre parece correcto y los paquetes de difusión no se pueden cifrar. En el envío uno por uno, en cambio, cada paquete recibe su propio ACK. Si pasas NULL como dirección a esp_now_send(), el paquete se envía por separado a cada par de la lista; si la dirección de difusión también está en la lista, el paquete sale además una vez como difusión. Una solución práctica para muchos proyectos es que las placas se encuentren por difusión en el primer arranque y después, conociendo ya sus direcciones MAC, pasen a unicast con confirmación.
Alcance y velocidad de ESP-NOW
| Métrica | Valor típico | Nota |
|---|---|---|
| Alcance en interiores | 20–40 m (1–3 paredes) | Las paredes de hormigón armado y los objetos metálicos son lo que más debilita la señal |
| Alcance en campo abierto | 100–200 m | Con la antena PCB de la placa; aumenta si las placas se colocan más altas |
| Modo LR (Long Range) | Hasta 1 km | Dato de Espressif con visión directa; la velocidad baja a 0,5 o 0,25 Mbps |
| Velocidad de datos por defecto | 1 Mbps | Más velocidad significa menos alcance |
| Tiempo en el aire de un paquete de 250 bytes | Unos 2,5 ms | Unos 3 ms por paquete contando el ACK y las esperas |
| Tasa teórica de paquetes | ~300 paquetes por segundo (~70 kB/s) | Con paquetes de 250 bytes; en la práctica es menor |
| Latencia de una orden | Pocos milisegundos | Sin establecer conexión ni servidor |
Estos valores varían mucho según el entorno, pero el cálculo es sencillo. A 1 Mbps, la velocidad por defecto, cada byte pasa 8 microsegundos en el aire; 250 bytes de datos con cabeceras y preámbulo tardan unos 2,5 ms. Con el ACK del receptor y la espera hasta que el canal queda libre, se necesitan unos 3 ms por paquete, es decir, como mucho unos 300 paquetes por segundo. Es más que suficiente para datos de sensores y órdenes, pero ESP-NOW no sirve para transmitir audio ni vídeo. Para ganar alcance, coloca las placas en alto, no tapes la antena con una caja metálica, baterías ni cables, usa módulos con antena externa (como el ESP32-WROOM-32U) y activa en los dos lados el modo de largo alcance (LR) de Espressif con esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR). LR solo funciona entre dos ESP32; una interfaz configurada solo en LR no puede conectarse a routers normales, y el ESP8266 y el ESP32-C2 no admiten este modo. Cuando pruebes el alcance con el core 3.x, imprime bilgi->rx_ctrl->rssi en el receptor: −50 dBm es una señal muy fuerte, −80 dBm está cerca del límite y hacia −90 dBm empiezan las pérdidas de paquetes.
Cambios en el core 3.x de Arduino para ESP32
La mayoría de los ejemplos de ESP-NOW en internet se escribieron para el core 2.x de Arduino para ESP32. Si instalaste la versión 3.x desde el Gestor de placas, ese código no compila, porque Espressif cambió la forma de las funciones callback con ESP-IDF 5. Los cambios llegaron en dos pasos: el core 3.0 cambió el callback de recepción y el core 3.3, basado en ESP-IDF 5.5, el de envío. Puedes ver qué versión tienes junto al paquete esp32 en Herramientas > Placa > Gestor de placas.
| Tema | Core 2.x | Core 3.0–3.2 | Core 3.3 y posteriores |
|---|---|---|---|
| Callback de recepción | (const uint8_t *mac, const uint8_t *veri, int uzunluk) | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) | Igual que 3.0 |
| Dirección del emisor | mac | bilgi->src_addr | bilgi->src_addr |
| Callback de envío | (const uint8_t *mac, esp_now_send_status_t durum) | Igual que 2.x | (const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) |
| Paquete más grande | 250 bytes | 250 bytes; 1470 bytes con v2 en 3.2 | 1470 bytes con v2 |
| Ajuste del canal | esp_wifi_set_channel() | Además WiFi.setChannel() | Igual que 3.0 |
| Biblioteca de clases incluida | Ninguna | ESP32_NOW.h (ESP_NOW_Peer, ESP_NOW_Serial) | ESP32_NOW.h con soporte de v2 |
Al compilar código antiguo en 3.x verás, para el callback de recepción, el error invalid conversion from 'void (*)(const uint8_t*, const uint8_t*, int)' ... to 'esp_now_recv_cb_t'. La solución es que el primer parámetro de la función sea const esp_now_recv_info_t * y leer la dirección del emisor en bilgi->src_addr. Al pasar al core 3.3 aparece un error parecido, esta vez para esp_now_send_cb_t; el primer parámetro del callback de envío debe ser const esp_now_send_info_t *, y la dirección del receptor está en bilgi->des_addr. Los sketches de esta guía eligen la forma correcta con la macro ESP_IDF_VERSION, así que compilan sin cambios en 2.x, 3.0–3.2 y 3.3+. El core 3.x también trajo una biblioteca basada en clases llamada ESP32_NOW.h: escribes onReceive() y onSent() en una clase derivada de ESP_NOW_Peer, y la clase ESP_NOW_Serial te permite usar un enlace ESP-NOW igual que Serial. Encontrarás sus ejemplos bajo ESP_NOW en el menú Archivo > Ejemplos. Nosotros usamos directamente las funciones de esp_now.h porque siguen siendo compatibles con el ESP8266 y con el código antiguo.
ESP-NOW y Wi-Fi a la vez: la coincidencia de canal
El ESP32 tiene una sola radio de 2,4 GHz, y esa radio solo puede trabajar en un canal a la vez. Cuando la placa se conecta a un router, el canal lo decide el router, no tú; si el router está en el canal 6, ESP-NOW también funciona en el canal 6. Por eso, todos los sensores que envíen paquetes a una pasarela conectada al Wi-Fi deben pasarse también al canal del router. Si los canales no coinciden, el emisor muestra "failed"; si añadiste el par con un canal distinto de cero, en el Monitor Serie aparece además el error "ESPNOW: Peer channel is not equal to the home channel, send fail!". Para un montaje sin problemas, sigue estas reglas:
- La pasarela se conecta primero al Wi-Fi, inicia ESP-NOW después e imprime en el Monitor Serie el canal del router con WiFi.channel().
- Los sensores no se conectan al router; encuentran el canal buscando el nombre de la red (WiFi.scanNetworks() y WiFi.channel(i)) o usan un canal fijo en el código. Para ello no necesitan la contraseña del Wi-Fi.
- Cambia el canal de tu router de "Automático" a un canal fijo (1, 6 u 11). En modo automático, si el router cambia de canal, los sensores pierden la pasarela.
- Desactiva el ahorro de energía en la pasarela con WiFi.setSleep(false). Por defecto, un ESP32 conectado a un router duerme la radio a intervalos y puede perder los paquetes ESP-NOW que lleguen en ese momento.
- Escribe en los sensores la dirección MAC STA de la pasarela; el sketch de la pasarela que verás a continuación la imprime en el Monitor Serie al arrancar.
El siguiente sketch de pasarela se conecta a tu red doméstica, guarda las lecturas de hasta cinco sensores y las muestra en una página web sencilla que puedes abrir desde el móvil en la misma red. Cambia el nombre de la red y la contraseña por los tuyos y, después de subirlo, abre en el navegador del móvil la dirección IP que aparece en el Monitor Serie:
1#include <WiFi.h>
2#include <WebServer.h>
3#include <esp_now.h>
4#include <esp_idf_version.h>
5
6const char *ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
7const char *sifre = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
8
9// Sensör kodundakiyle birebir aynı / identical to the sensor sketch
10typedef struct __attribute__((packed)) {
11 uint8_t id; // sensör numarası 1-5 / sensor number 1-5
12 uint32_t sayac;
13 float sicaklik;
14 float nem;
15 float pil; // pil gerilimi (V) / battery voltage (V)
16} SensorPaketi;
17
18const int SENSOR_SAYISI = 5;
19SensorPaketi sonVeri[SENSOR_SAYISI + 1]; // 1..5 kullanılır / indexes 1..5 are used
20unsigned long sonZaman[SENSOR_SAYISI + 1]; // son paketin zamanı / time of the last packet
21QueueHandle_t kuyruk;
22WebServer server(80);
23
24#if ESP_IDF_VERSION_MAJOR >= 5
25void veriGeldi(const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) { // çekirdek 3.x / core 3.x
26#else
27void veriGeldi(const uint8_t *mac, const uint8_t *veri, int uzunluk) { // çekirdek 2.x / core 2.x
28#endif
29 if (uzunluk != sizeof(SensorPaketi)) return;
30 SensorPaketi p;
31 memcpy(&p, veri, sizeof(p));
32 xQueueSend(kuyruk, &p, 0); // kuyruğa at, Wi-Fi görevini bekletme / queue it, never block the Wi-Fi task
33}
34
35void anaSayfa() {
36 String html = "<meta name='viewport' content='width=device-width'>"
37 "<meta http-equiv='refresh' content='10'>"
38 "<h2>ESP-NOW sensorleri</h2><table border=1 cellpadding=6>"
39 "<tr><th>ID</th><th>Sicaklik</th><th>Nem</th><th>Pil</th><th>Son paket</th></tr>";
40 for (int i = 1; i <= SENSOR_SAYISI; i++) {
41 if (sonZaman[i] == 0) continue; // bu sensörden veri yok / no data from this sensor yet
42 html += "<tr><td>" + String(i) + "</td><td>" + String(sonVeri[i].sicaklik, 1) +
43 " °C</td><td>%" + String(sonVeri[i].nem, 0) + "</td><td>" + String(sonVeri[i].pil, 2) +
44 " V</td><td>" + String((millis() - sonZaman[i]) / 1000) + " sn once</td></tr>";
45 }
46 html += "</table>";
47 server.send(200, "text/html", html);
48}
49
50void setup() {
51 Serial.begin(115200);
52 kuyruk = xQueueCreate(10, sizeof(SensorPaketi));
53
54 WiFi.mode(WIFI_STA);
55 WiFi.begin(ssid, sifre);
56 while (WiFi.status() != WL_CONNECTED) delay(500);
57 WiFi.setSleep(false); // güç tasarrufu kapalı: paket kaçmaz / power save off: no missed packets
58
59 Serial.print("IP: ");
60 Serial.println(WiFi.localIP());
61 Serial.printf("Kanal / channel: %d\n", (int)WiFi.channel());
62 Serial.print("Gecit MAC / gateway MAC: ");
63 Serial.println(WiFi.macAddress()); // sensörlere bu adresi yaz / put this address in the sensors
64
65 if (esp_now_init() != ESP_OK) {
66 Serial.println("ESP-NOW baslatilamadi / ESP-NOW init failed");
67 return;
68 }
69 esp_now_register_recv_cb(veriGeldi);
70
71 server.on("/", anaSayfa);
72 server.begin();
73}
74
75void loop() {
76 SensorPaketi p;
77 while (xQueueReceive(kuyruk, &p, 0) == pdTRUE) {
78 if (p.id >= 1 && p.id <= SENSOR_SAYISI) {
79 sonVeri[p.id] = p;
80 sonZaman[p.id] = millis();
81 Serial.printf("Sensor %u: %.1f C %%%.0f %.2f V\n", (unsigned)p.id, p.sicaklik, p.nem, p.pil);
82 }
83 }
84 server.handleClient();
85}En el callback de recepción, la pasarela solo mete el paquete en una cola (queue) de FreeRTOS y sale enseguida. loop() vacía la cola y guarda la última lectura y la hora de llegada de cada sensor, y el servidor web muestra esa tabla. La cola garantiza que no se pierda ningún paquete cuando varios sensores envían a la vez; además es la forma práctica de seguir el consejo de Espressif de no hacer trabajos largos dentro de los callbacks. La página se recarga sola cada 10 segundos, y la columna "Son paket" (último paquete) muestra al momento si un sensor ha dejado de enviar (¿batería agotada o fuera de alcance?). Si la pasarela no consigue conectarse al Wi-Fi, el problema no es ESP-NOW; repasa las comprobaciones de el ESP32 no se conecta al Wi-Fi. Si quieres enviar los datos a un sistema de domótica, puedes usar MQTT y Home Assistant en lugar de la página web.
Ejemplo de sensor remoto: DHT22, sueño profundo y ESP-NOW
Ahora vamos a construir el sensor a batería que envía datos a la pasarela. Cada vez que se despierta, el sensor mide la temperatura, la humedad y la tensión de la batería, envía un único paquete ESP-NOW, espera la confirmación de entrega y entra en sueño profundo durante 60 segundos. Para la temperatura y la humedad usamos un DHT22; tienes que instalar la DHT sensor library de Adafruit desde el Gestor de bibliotecas. Encontrarás cómo funciona el sensor en nuestra guía del sensor de temperatura y humedad DHT11; el cableado es el mismo para el DHT22 y en el código solo indicas DHT22 como tipo de sensor.




| Conexión | Pin del ESP32 | Nota |
|---|---|---|
| DHT22 VCC (+) | 3V3 | Funciona con 3,3–5 V |
| DHT22 DATA | GPIO 4 | Con el sensor sin módulo, resistencia de 10 kΩ entre DATA y 3V3 |
| DHT22 GND (−) | GND | Tierra común |
| Positivo de la batería | GPIO 34 (a través del divisor) | Dos resistencias de 100 kΩ; una batería de 4,2 V llega al pin como 2,1 V |
| Negativo de la batería | GND | La resistencia inferior del divisor también va a GND |
1#include <WiFi.h>
2#include <esp_now.h>
3#include <esp_wifi.h>
4#include <esp_idf_version.h>
5#include <DHT.h>
6
7const char *AG_ADI = "AgAdi"; // ağ geçidinin bağlandığı Wi-Fi (şifre gerekmez) / Wi-Fi the gateway uses (no password needed)
8uint8_t gecitMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC}; // ağ geçidinin STA MAC adresi / gateway STA MAC
9const uint8_t SENSOR_ID = 1; // her sensöre farklı numara / a different number for each sensor
10const uint64_t UYKU_US = 60ULL * 1000000ULL; // 60 saniye uyku / sleep for 60 seconds
11
12#define DHT_PIN 4
13#define PIL_PIN 34 // ADC1 pini, 100k + 100k bölücü / ADC1 pin, 100k + 100k divider
14DHT dht(DHT_PIN, DHT22);
15
16typedef struct __attribute__((packed)) {
17 uint8_t id;
18 uint32_t sayac;
19 float sicaklik;
20 float nem;
21 float pil;
22} SensorPaketi;
23
24RTC_DATA_ATTR uint8_t kanal = 0; // derin uykuda silinmez / kept during deep sleep
25RTC_DATA_ATTR uint32_t paketSayaci = 0;
26volatile bool sonucGeldi = false;
27volatile bool teslimEdildi = false;
28
29#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
30void gonderimSonucu(const esp_now_send_info_t *bilgi, esp_now_send_status_t durum) { // çekirdek 3.3+ / core 3.3+
31#else
32void gonderimSonucu(const uint8_t *mac, esp_now_send_status_t durum) { // çekirdek 2.x-3.2 / core 2.x-3.2
33#endif
34 teslimEdildi = (durum == ESP_NOW_SEND_SUCCESS);
35 sonucGeldi = true;
36}
37
38uint8_t kanalBul(const char *ag) { // ağın kanalını tarayarak bul / find the network's channel by scanning
39 int n = WiFi.scanNetworks();
40 for (int i = 0; i < n; i++) {
41 if (WiFi.SSID(i) == ag) return WiFi.channel(i);
42 }
43 return 1; // bulunamazsa kanal 1 / channel 1 if not found
44}
45
46void uyu() {
47 esp_sleep_enable_timer_wakeup(UYKU_US);
48 esp_deep_sleep_start();
49}
50
51void setup() {
52 Serial.begin(115200);
53 dht.begin();
54 WiFi.mode(WIFI_STA);
55
56 if (kanal == 0) kanal = kanalBul(AG_ADI); // yalnız ilk açılışta ya da hatadan sonra tara / scan only at first boot or after a failure
57 esp_wifi_set_promiscuous(true);
58 esp_wifi_set_channel(kanal, WIFI_SECOND_CHAN_NONE);
59 esp_wifi_set_promiscuous(false);
60
61 if (esp_now_init() != ESP_OK) uyu();
62 esp_now_register_send_cb(gonderimSonucu);
63
64 esp_now_peer_info_t es = {};
65 memcpy(es.peer_addr, gecitMac, 6);
66 es.channel = kanal;
67 es.ifidx = WIFI_IF_STA;
68 esp_now_add_peer(&es);
69
70 SensorPaketi p;
71 p.id = SENSOR_ID;
72 p.sayac = ++paketSayaci;
73 p.sicaklik = dht.readTemperature();
74 p.nem = dht.readHumidity();
75 p.pil = analogReadMilliVolts(PIL_PIN) * 2 / 1000.0; // bölücü yarıya indirir / the divider halves it
76
77 esp_now_send(gecitMac, (uint8_t *)&p, sizeof(p));
78 unsigned long baslangic = millis();
79 while (!sonucGeldi && millis() - baslangic < 200) delay(1); // en fazla 200 ms bekle / wait up to 200 ms
80
81 Serial.printf("Kanal %u, %.1f C, %%%.0f, %.2f V, %s\n", (unsigned)kanal, p.sicaklik, p.nem, p.pil,
82 teslimEdildi ? "teslim edildi / delivered" : "teslim edilemedi / failed");
83 if (!teslimEdildi) kanal = 0; // teslim edilemedi: sonraki uyanışta yeniden tara / not delivered: rescan next wake
84 Serial.flush();
85 uyu();
86}
87
88void loop() {
89}La clave de este sketch son las variables marcadas con RTC_DATA_ATTR. En sueño profundo, la memoria principal del ESP32 se borra, pero estas variables de la memoria RTC se conservan. El sensor solo busca el canal escaneando en el primer arranque (tarda unos 2 segundos) y en los siguientes despertares usa directamente ese canal; si un paquete no se entrega, pone a cero la variable del canal y vuelve a escanear en el siguiente despertar. Así, aunque el router cambie de canal, el sensor se recupera al ciclo siguiente. La tensión de la batería se lee ya calibrada con analogReadMilliVolts() y se multiplica por 2 porque el divisor la reduce a la mitad; puedes comprobar los valores de las resistencias con la herramienta Divisor de tensión. GPIO 34 es un pin ADC1; esto es importante porque los pines ADC2 no pueden hacer lecturas analógicas con la radio Wi-Fi encendida. Tienes los detalles de los modos de sueño en nuestra guía ESP32 Deep Sleep.
Calculemos a grandes rasgos la duración de la batería. Un despertar sin escaneo (arranque, medición y envío) dura unos 0,3 segundos con un consumo medio de unos 100 mA, lo que da aproximadamente 0,008 mAh por despertar. Con un despertar por minuto salen 1440 despertares al día y unos 12 mAh en total. Un módulo ESP32 sin placa de desarrollo consume unos 10 µA en sueño profundo; sumando la corriente en reposo del DHT22 y las resistencias del divisor, el consumo en sueño se queda por debajo de 2 mAh al día. Así que el sensor gasta unos 14 mAh diarios y una pila 18650 de 2000 mAh dura de 4 a 5 meses. Para hacer el cálculo con tus propios valores, usa la calculadora de duración de batería.
ESP-NOW con el ESP8266

Las placas ESP8266 pueden usar ESP-NOW en la misma red que las ESP32, pero su biblioteca de Arduino es distinta: se incluye espnow.h en lugar de esp_now.h y ESP8266WiFi.h en lugar de WiFi.h, y las funciones vienen del SDK del ESP8266, no de ESP-IDF. Las diferencias principales son:
| Tarea | ESP32 (esp_now.h) | ESP8266 (espnow.h) |
|---|---|---|
| Inicializar | esp_now_init() == ESP_OK | esp_now_init() == 0 |
| Elegir rol | No hace falta | esp_now_set_self_role(ESP_NOW_ROLE_COMBO) |
| Añadir un par | esp_now_add_peer(&es) con un struct | esp_now_add_peer(mac, ESP_NOW_ROLE_COMBO, canal, NULL, 0) |
| Callback de recepción | (const esp_now_recv_info_t *bilgi, const uint8_t *veri, int uzunluk) | (uint8_t *mac, uint8_t *datos, uint8_t longitud) |
| Callback de envío | (..., esp_now_send_status_t durum) | (uint8_t *mac, uint8_t estado); 0 significa éxito |
| Paquete más grande | 250 bytes (1470 con v2) | 250 bytes |
En el ESP8266, no olvides añadir WiFi.disconnect() después de WiFi.mode(WIFI_STA); al arrancar, el ESP8266 intenta conectarse por su cuenta al último router que usó, el canal cambia y se pueden perder paquetes. Si defines el struct de datos como packed en ambos lados, el ESP32 y el ESP8266 leen los mismos bytes de la misma forma, porque los dos son procesadores de 32 bits little-endian. Para los pines y la configuración del ESP8266, consulta nuestra guía ESP8266 NodeMCU.
ESP-NOW con MicroPython y la Raspberry Pi Pico
Si programas el ESP32 con MicroPython, existe un módulo espnow listo para ESP-NOW; viene incluido en las versiones de MicroPython para ESP32 y ESP8266. El siguiente código recibe el struct Paket de 30 bytes que envía el ejemplo emisor de Arduino y lo decodifica con struct.unpack(). En la cadena de formato, < indica little-endian sin bytes de relleno; B es un número de 1 byte, I un número sin signo de 4 bytes, f un float y 16s un texto de 16 bytes. El campo bool de Arduino se lee con B como 0 o 1. Sube el código a la placa con Thonny y escribe la dirección MAC que imprime en el array aliciMac del emisor de Arduino:
1import network
2import espnow
3import struct
4import binascii
5
6sta = network.WLAN(network.STA_IF) # ESP-NOW istasyon arayüzünü kullanır / ESP-NOW uses the station interface
7sta.active(True)
8print("MAC:", binascii.hexlify(sta.config("mac"), ":").decode()) # bu adresi vericiye yaz / put it in the sender
9
10e = espnow.ESPNow()
11e.active(True)
12
13BICIM = "<BIffB16s" # Arduino'daki Paket yapısı / the Paket struct from Arduino
14BOYUT = struct.calcsize(BICIM) # 30 bayt / 30 bytes
15
16while True:
17 mac, msg = e.recv() # paket gelene kadar bekle / wait for a packet
18 if msg and len(msg) == BOYUT:
19 kimlik, sayac, sicaklik, nem, buton, mesaj = struct.unpack(BICIM, msg)
20 metin = mesaj.split(b"\x00")[0].decode()
21 print(binascii.hexlify(mac, ":").decode(), kimlik, sayac,
22 round(sicaklik, 1), round(nem, 1), bool(buton), metin)Enviar un paquete desde MicroPython también lleva dos líneas: añade el receptor con e.add_peer(mac) y envía el mismo struct con e.send(mac, struct.pack(BICIM, 2, 1, 23.5, 45.0, 0, b"Selam")); así, placas con Arduino y con MicroPython trabajan juntas en la misma red. En una placa MicroPython conectada al Wi-Fi, desactiva el ahorro de energía con sta.config(pm=sta.PM_NONE) para no perder paquetes. Si MicroPython es nuevo para ti, empieza por nuestra guía de fundamentos de MicroPython. La familia Raspberry Pi Pico, en cambio, no tiene ESP-NOW: la Pico estándar no tiene radio, y el chip CYW43439 de la Pico W y la Pico 2 W solo ejecuta Wi-Fi y Bluetooth estándar. Para incluir una Pico en una red ESP-NOW, conéctale un pequeño ESP32-C3 mediante comunicación serie UART y úsalo como puente, o haz que la Pico W hable con la pasarela por Wi-Fi; el Wi-Fi de la Pico W se explica en Pico W con Wi-Fi y servidor web. El mismo método del puente sirve para el Arduino Uno.
Cifrar los paquetes ESP-NOW
Por defecto, los paquetes ESP-NOW no van cifrados; cualquiera en el mismo canal puede capturarlos en el aire y enviar paquetes falsos que imiten tu struct. Usa cifrado en proyectos como cerraduras o puertas de garaje. ESP-NOW usa CCMP, igual que WPA2, y trabaja con dos claves: una PMK de 16 bytes que es igual en todos los dispositivos (se configura con esp_now_set_pmk()) y una LMK de 16 bytes distinta para cada par. Al añadir el par, pones es.encrypt = true y copias la clave en es.lmk; el otro lado también debe añadirte como par cifrado con la misma LMK. Para recibir paquetes cifrados, el receptor tiene que añadir al emisor a su lista. Los paquetes de difusión no se pueden cifrar y, por defecto, solo puede haber 7 pares cifrados como máximo. El cifrado por sí solo no impide que un paquete capturado se reenvíe más tarde; añadir un contador creciente a las órdenes críticas y rechazar en el receptor los números antiguos es una buena protección extra.
Ideas de proyectos con ESP-NOW
- Timbre inalámbrico: Al pulsar el botón del ESP32 de la puerta, suena un zumbador en la placa de dentro de casa; para alargar la batería, usa el botón como pin que despierta la placa del sueño profundo.
- Temperatura en varias habitaciones: Pon un sensor DHT22 o BME280 en cada habitación, reúne las lecturas en una pasarela y compáralas en una página web.
- Riego del jardín: Un sensor de humedad del suelo envía la orden "regar" a la placa junto a la bomba; para el cableado consulta sensor de humedad del suelo.
- Mando inalámbrico para robot: Envía los valores de un joystick 20 veces por segundo; para la parte mecánica del robot, mira cómo hacer un coche robot con Arduino.
- Alarma de jardín: Cuando un sensor PIR del jardín detecta movimiento, la pasarela envía un aviso por Telegram.
- Espectáculo de luces sincronizado: Con un solo paquete de difusión, cambia a la vez al mismo color las tiras WS2812B de varias placas.
Errores comunes de ESP-NOW y soluciones
| Síntoma / error | Causa | Solución |
|---|---|---|
| "ESPNOW: Peer interface is invalid", no se puede añadir el par | Se usó esp_now_peer_info_t sin ponerla a cero | Pon la estructura a cero con = {} o memset y escribe ifidx = WIFI_IF_STA |
| "failed" constante | MAC equivocada (dirección AP o errata), receptor apagado o en otro canal | Vuelve a leer la MAC STA del receptor y comprueba el canal de las dos placas |
| Llegan paquetes pero los valores no tienen sentido | El struct es distinto en cada lado o contiene un String | Define el struct igual y packed en ambos lados y usa un array char para el texto |
| "invalid conversion ... to 'esp_now_recv_cb_t'" | Código escrito para el core 2.x compilado en 3.x | Escribe el callback de recepción con const esp_now_recv_info_t * |
| "invalid conversion ... to 'esp_now_send_cb_t'" | Código escrito para el core 3.0–3.2 compilado en 3.3+ | Escribe el callback de envío con const esp_now_send_info_t * o usa la macro ESP_IDF_VERSION |
| "Peer channel is not equal to the home channel, send fail!" | El canal de la placa conectada al Wi-Fi es distinto del canal del par | Añade el par con channel = 0 y pasa los sensores al canal del router |
| La pasarela conectada al Wi-Fi pierde algunos paquetes | Ahorro de energía del Wi-Fi (modem sleep) | Usa WiFi.setSleep(false) |
| ESP_ERR_ESPNOW_NOT_INIT | No se llamó a esp_now_init() o se llamó antes de iniciar el Wi-Fi | Primero WiFi.mode(WIFI_STA) y después esp_now_init() |
| ESP_ERR_ESPNOW_FULL | La lista de pares llegó a 20 entradas | Borra pares antiguos con esp_now_del_peer() o usa la difusión |
| ESP_ERR_ESPNOW_NO_MEM | Se envían paquetes seguidos antes de que se vacíe la cola de envío | Espera el resultado del envío antes de mandar el siguiente paquete |
| La dirección MAC sale como 00:00:00:00:00:00 | En el core 3.x se llamó a WiFi.macAddress() antes de que el Wi-Fi arrancara del todo | Usa esp_wifi_get_mac() después de WiFi.mode(WIFI_STA) |
ESP-NOW es la forma más rápida de hacer que varios ESP32 se comuniquen sin router, cables ni módulos extra: lee la dirección MAC, define el struct de datos igual en los dos lados, añade el par y envía. Cuando lo uses junto con el Wi-Fi, recuerda la única regla: todos deben estar en el mismo canal. En la app BlueGrays puedes probar todas las piezas de esta guía, desde el control de LED y botón con el ESP32 hasta la medición de temperatura con el DHT22, la conexión Wi-Fi, el servidor web y el sueño profundo, paso a paso con esquemas de circuito y código listo para usar.
Preguntas frecuentes
¿ESP-NOW necesita router o internet?
No. ESP-NOW usa la radio Wi-Fi de las placas, pero no se une a ninguna red; las placas se envían paquetes directamente a sus direcciones MAC. Si quieres llevar los datos a internet, necesitas un ESP32 que haga de pasarela conectado a la vez al Wi-Fi y a ESP-NOW.
¿Cuál es el alcance de ESP-NOW?
Con placas ESP32 con antena integrada se suelen conseguir 20–40 metros en interiores, según las paredes, y unos 100–200 metros en campo abierto. Con módulos de antena externa y el modo LR (Long Range) activado en ambos lados, Espressif indica que se puede llegar hasta 1 kilómetro con visión directa.
¿Cuántos dispositivos se pueden conectar con ESP-NOW?
La lista de pares de una placa admite hasta 20 dispositivos, y por defecto como máximo 7 de ellos pueden ir cifrados. Este límite es para los dispositivos a los que envías: una pasarela puede escuchar a más de 20 sensores que envíen sin cifrar, y una difusión llega con un solo paquete a todos los dispositivos del mismo canal.
¿Se pueden usar ESP-NOW y Wi-Fi a la vez?
Sí, pero como la única radio del ESP32 trabaja en un canal cada vez, todos los dispositivos ESP-NOW deben usar el canal del router. Desactiva el ahorro de energía de la pasarela con WiFi.setSleep(false) y, en los sensores, busca el canal escaneando el nombre de la red o fija el canal del router.
¿Pueden comunicarse un ESP32 y un ESP8266 por ESP-NOW?
Sí. Los dos chips admiten ESP-NOW y pueden enviarse paquetes; en el ESP8266 solo cambian la biblioteca (espnow.h) y la forma de las funciones. Define el struct de datos idéntico y packed en ambos lados y mantén los paquetes por debajo de 250 bytes.
¿Cuántos bytes se pueden enviar como máximo con ESP-NOW?
Con ESP-NOW v1 caben como máximo 250 bytes por paquete, y este límite se aplica a todas las placas, incluido el ESP8266. Las placas con ESP-IDF 5.4 o posterior (core 3.2+ de Arduino) pueden enviar hasta 1470 bytes con ESP-NOW v2, pero las placas con v1 no reciben paquetes de más de 250 bytes.
Proyectos para hacer después de leer esto
Ver todos los proyectos
LED Parpadeante con ESP32
Enseña el uso básico de la salida GPIO con ESP32. Conseguimos un efecto de parpadeo encendiendo y apagando un LED desde el pin digital. Versión para ESP32 del clásico proyecto blink en Arduino UNO; los comandos básicos de blink en el entorno Arduino son los mismos, pero la distribución de pines y los niveles GPIO de 3,3 V del ESP32 difieren de los del Arduino UNO.

Control de LED con botón en ESP32 (Toggle)
Cada vez que se pulsa el botón, cambia el estado del LED: si está encendido se apaga, si está apagado se enciende. Patrón de conmutación clásico. Con debounce limpiamos el ruido mecánico del pulsador para garantizar una sola conmutación con una sola pulsación.

Comunicación bidireccional entre 2 Arduino
Dos Arduino se conectan con cables y hablan entre sí: pulsas el botón de uno y se enciende la luz del otro, lo pulsas otra vez y se apaga. Las dos placas llevan el mismo programa, así que cada una env

ESP32 DHT22 Temperatura y humedad (con pantalla LCD)
Se trata de un proyecto que mide la temperatura y la humedad ambientales mediante el sensor DHT22 y las muestra en tiempo real en una pantalla LCD I2C 16x2. Los datos del sensor se leen mediante el protocolo de un solo cable (one-wire) utilizando la biblioteca DHT de Adafruit. Este proyecto constituye la base para estaciones meteorológicas IoT, sistemas de automatización doméstica inteligente o sistemas de monitorización ambiental en invernaderos.

Conexión Wi-Fi STA con ESP32 (con pantalla LCD)
En este proyecto, conectamos el ESP32 a la red Wi-Fi doméstica en modo Station (STA) de forma segura y tolerante a fallos. Separamos el SSID y la contraseña en el archivo secrets.h para evitar que se filtren a Git, configuramos un mecanismo de reconexión automática y mostramos el nivel de señal RSSI y la dirección IP en una pantalla LCD I2C. El LED interno actúa como indicador de estado.

Servidor web asíncrono ESP32 (con indicación de IP en la pantalla LCD)
En este proyecto, convertimos el ESP32 en un servidor web asíncrono que puedes controlar desde tu teléfono o tu ordenador, siempre que estén conectados a la misma red Wi-Fi. Gracias a la biblioteca ESPAsyncWebServer, el servidor puede procesar varias solicitudes al mismo tiempo (a diferencia del clásico WebServer.h, que se bloquea con una sola solicitud).

Modo de suspensión profunda del ESP32 — Ahorro de batería (OLED + contador en RTC RAM)
El modo «Deep Sleep» del ESP32 reduce el consumo de corriente a unos 10 µA al desactivar la CPU y la mayoría de los periféricos, lo cual es de vital importancia para los dispositivos IoT que funcionan con pilas. En este proyecto utilizamos tres fuentes de activación diferentes: el temporizador (15 s), un pin externo (EXT0 — botón GPIO 33) y el sensor táctil (T0/GPIO 4). Las variables marcadas con RTC_DATA_ATTR se conservan durante el modo Deep Sleep: el contador de arranque, el contador de act

Registrador de Temperatura ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
¡Construye un registrador de temperatura con marca de tiempo con ESP32! Lee temperatura/humedad del DHT22 cada minuto, obtén la hora real del DS1307 RTC y registra en tarjeta microSD como CSV. El OLED muestra en vivo la temperatura (fuente grande), porcentaje de humedad, marca de tiempo y contador de registros. El archivo /temp_log.csv en la tarjeta SD se abre en Excel, Google Sheets o Python pandas para análisis — perfecto para monitoreo de invernadero, seguimiento de temperatura del hog
Más artículos
App BlueGrays
Monta este circuito paso a paso en tu teléfono
Esquema, código y lista de piezas en una sola pantalla. Más de 200 proyectos de Arduino, ESP32 y Pico listos para montar.
Escanea con tu teléfono
Apunta la cámara al código y se abrirá la tienda de apps de tu teléfono.
o ábrela en la tienda

