MER · Radiocomunicaciones
ESP-NOW: inalámbrico sin router
Protocolo propietario de Espressif para redes locales rápidas y de bajo consumo entre placas ESP32, identificadas por su dirección MAC.
Cumplimiento de la tarea
Resumen de lo que pide la práctica y dónde está resuelto en esta página.
| Requisito | Dónde | Contenido | |
|---|---|---|---|
| Qué es ESP-NOW | Sección 02 | Protocolo propietario de Espressif, sin router | ✔ |
| Cómo funciona | Sección 03 | Tramas de acción, peers, ACK, callbacks + esquema SVG | ✔ |
| Para qué sirve | Sección 07 | Casos de uso y comparativa (04, 06) | ✔ |
| Sin router / MAC / latencia | Sección 02 · 04 | Identificación por MAC, latencia baja | ✔ |
| Topología muchos a 1 y muchos a muchos | Sección 05 | Ambas explicadas con diagrama SVG | ✔ |
| Esquema / diagrama SVG | Sección 03 · 05 | Secuencia, estructura de trama y 4 topologías | ✔ |
| Código .ino | Sección 08 | 4 sketches en la carpeta codigo/ | ✔ |
| Capturas Monitor Serie | Sección 09 | Salida de ejemplo (no es captura real) | ✔ |
Qué es ESP-NOW
ESP-NOW es un protocolo de comunicación inalámbrica propietario de Espressif que permite que varias placas ESP32/ESP8266 intercambien datos directamente, sin router ni red WiFi. Funciona en la banda de 2,4 GHz reutilizando la radio WiFi, pero sin el proceso de conexión (asociación, DHCP, TCP) de una red normal: es sin conexión (connectionless).
- Sin router: no hace falta infraestructura; las placas hablan entre sí.
- Identificación por MAC: cada placa se direcciona con su dirección MAC de 6 bytes (p. ej.
24:6F:28:AA:BB:CC). - Latencia mínima: al no haber conexión previa, el mensaje se envía en cuanto se llama a
esp_now_send(). Ideal para control y sensores. - Bajo consumo: la radio solo se usa para el instante del envío.
Cómo funciona
ESP-NOW encapsula los datos en tramas de acción específicas del fabricante (vendor-specific action frames) del estándar 802.11. Cada mensaje lleva hasta 250 bytes y el receptor responde con un ACK de capa de enlace, de forma automática.
- Inicializar:
WiFi.mode(WIFI_STA)yesp_now_init(). - Registrar peers: con
esp_now_add_peer()se da de alta la MAC de cada destino (canal y cifrado opcional). - Enviar:
esp_now_send(mac, datos, longitud). El resultado llega al callback de envío. - Recibir:
esp_now_register_recv_cb()llama a tu función con la MAC de origen y los datos.
Estructura de la trama
channel = 0 se usa el canal actual de la placa (por defecto el 1). El cifrado opcional (CCMP) admite hasta 6 peers cifrados.Características principales
| Característica | Valor |
|---|---|
| Banda | 2,4 GHz (IEEE 802.11) |
| Tamaño de mensaje | Hasta 250 bytes (v1.0); versiones nuevas de ESP-IDF admiten más |
| Peers | Hasta 20 en total (6 con cifrado) |
| Modos | Unicast (con ACK) y broadcast (sin ACK) |
| Cifrado | Opcional, CCMP con claves PMK/LMK de 16 bytes |
| Direccionamiento | Dirección MAC de 6 bytes |
| Conexión | Sin conexión: no hay handshake ni asociación |
| Latencia | Muy baja (orientativo: milisegundos), sin negociar red |
| Alcance | Decenas de metros en interior; más en campo abierto (orientativo, depende de antena y entorno) |
| Coexistencia | Puede usarse a la vez que WiFi STA si comparten canal |
Topologías
Selecciona una pestaña. Las líneas animadas muestran el sentido de los datos. Las pedidas por la tarea son muchos a 1 y muchos a muchos.
Un emisor y un receptor. Es la base: se registra la MAC del otro como peer y se llama a esp_now_send().
Un emisor reparte a varios receptores: con la MAC de difusión FF:FF:FF:FF:FF:FF la reciben todos a la vez (sin ACK), o con varias MAC en unicast (con ACK, máx. 20 peers).
Topología pedida. Varios sensores (emisores) mandan al mismo receptor, que actúa como “pasarela”. Cada emisor solo registra una MAC (la del receptor). El receptor distingue quién envía con la MAC de origen que llega al callback y con un campo id en la estructura. Ideal para una red de sensores (temperatura, humedad…) que se concentra en un nodo.
Topología pedida. Todos los nodos son a la vez emisores y receptores. Se programa el mismo sketch en todas las placas y se usa broadcast (o cada placa añade como peers a las demás). Útil para mallas pequeñas, mandos distribuidos o alarmas compartidas.
Comparativa
| ESP-NOW | WiFi con router | Bluetooth LE | |
|---|---|---|---|
| Necesita router | No | Sí | No (emparejamiento) |
| Dirección | MAC | IP | MAC / GATT |
| Establecer conexión | No | Sí (asociación + DHCP) | Sí (conexión) |
| Latencia | Muy baja | Media | Baja |
| Tamaño de dato | ≤ 250 B | Grande (TCP/HTTP) | Pequeño (~20-512 B) |
| Acceso a Internet | No directo | Sí | No |
| Mejor uso | Sensores locales y control | Servicios web / nube | Móvil ↔ dispositivo |
Para qué sirve
Se relaciona con el módulo MER (radiocomunicaciones): ESP-NOW usa la misma banda ISM de 2,4 GHz que WiFi y Bluetooth, por lo que comparte canal e interferencias con ellas. Puede combinarse con el proyecto ESP32 + DHT22: varios nodos miden y un nodo pasarela, conectado a WiFi, sube los datos a Internet.
Ejemplo práctico (emisor + receptor)
Cuatro sketches de Arduino para ESP32, también incluidos como archivos .ino en la carpeta codigo/ junto a esta página.
- Sube
mac_address.inoa la placa receptora y anota su MAC. - Pega esa MAC en
macReceptor[]deemisor_muchos_a_1.ino. - Sube el emisor a cada placa cambiando
ID_EMISOR(1, 2, 3…). - Sube
receptor_muchos_a_1.inoa la receptora y abre el Monitor Serie a 115200 baudios.
codigo/mac_address/mac_address.ino// mac_address.ino · Muestra la dirección MAC (modo estación) de la placa
// Cada placa ESP32 se identifica en ESP-NOW por esta dirección.
#include <WiFi.h>
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA); // ESP-NOW trabaja con la interfaz de estación
delay(100);
Serial.println();
Serial.print("MAC de esta placa: ");
Serial.println(WiFi.macAddress());
}
void loop() {
}
codigo/receptor_muchos_a_1/receptor_muchos_a_1.ino// receptor_muchos_a_1.ino · Receptor ESP-NOW (recibe de varios emisores)
#include <WiFi.h>
#include <esp_now.h>
// Estructura IDÉNTICA en emisores y receptor
typedef struct {
uint8_t id; // número del emisor (1, 2, 3...)
float temperatura; // °C
float humedad; // %
uint32_t contador; // nº de mensaje
} Mensaje;
Mensaje datos;
void procesar(const uint8_t *mac, const uint8_t *entrada, int longitud) {
if (longitud != sizeof(Mensaje)) {
Serial.printf("Paquete ignorado (tamaño %d, se esperaban %d)\n", longitud, (int)sizeof(Mensaje));
return;
}
memcpy(&datos, entrada, sizeof(Mensaje));
Serial.printf("Emisor %u (%02X:%02X:%02X:%02X:%02X:%02X) | T=%.1f C | H=%.1f %% | msg #%lu\n",
datos.id, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
datos.temperatura, datos.humedad, (unsigned long)datos.contador);
}
// La firma del callback cambió en el núcleo 3.x del ESP32 para Arduino
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
void alRecibir(const esp_now_recv_info_t *info, const uint8_t *entrada, int longitud) {
procesar(info->src_addr, entrada, longitud);
}
#else
void alRecibir(const uint8_t *mac, const uint8_t *entrada, int longitud) {
procesar(mac, entrada, longitud);
}
#endif
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
delay(100);
Serial.print("Receptor listo. Mi MAC: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != ESP_OK) {
Serial.println("Error al iniciar ESP-NOW");
return;
}
esp_now_register_recv_cb(alRecibir);
}
void loop() {
// Todo ocurre en el callback alRecibir()
}
codigo/emisor_muchos_a_1/emisor_muchos_a_1.ino// emisor_muchos_a_1.ino · Emisor ESP-NOW (envía al receptor por su MAC)
#include <WiFi.h>
#include <esp_now.h>
// 1) Cambia por la MAC de TU receptor (la que mostró mac_address.ino)
uint8_t macReceptor[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};
// 2) Número de este emisor (1, 2, 3...). Cambia en cada placa emisora
#define ID_EMISOR 1
typedef struct {
uint8_t id;
float temperatura;
float humedad;
uint32_t contador;
} Mensaje;
Mensaje datos;
unsigned long ultimoEnvio = 0;
const unsigned long INTERVALO = 2000; // ms
void mostrarEstado(esp_now_send_status_t estado) {
Serial.print("Estado del envío: ");
Serial.println(estado == ESP_NOW_SEND_SUCCESS ? "ENTREGADO" : "FALLO");
}
// La firma del callback de envío depende de la versión del núcleo
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 3, 0)
void alEnviar(const wifi_tx_info_t *info, esp_now_send_status_t estado) {
mostrarEstado(estado);
}
#else
void alEnviar(const uint8_t *mac, esp_now_send_status_t estado) {
mostrarEstado(estado);
}
#endif
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
delay(100);
Serial.print("Emisor ");
Serial.print(ID_EMISOR);
Serial.print(" listo. Mi MAC: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != ESP_OK) {
Serial.println("Error al iniciar ESP-NOW");
return;
}
esp_now_register_send_cb(alEnviar);
// Registrar al receptor como "peer" (pareja)
esp_now_peer_info_t peer;
memset(&peer, 0, sizeof(peer));
memcpy(peer.peer_addr, macReceptor, 6);
peer.channel = 0; // 0 = el canal actual de la placa
peer.encrypt = false; // sin cifrado en este ejemplo
if (esp_now_add_peer(&peer) != ESP_OK) {
Serial.println("No se pudo añadir el receptor");
}
}
void loop() {
if (millis() - ultimoEnvio >= INTERVALO) {
ultimoEnvio = millis();
datos.id = ID_EMISOR;
datos.temperatura = 20.0 + random(0, 100) / 10.0; // valor simulado (sustituir por el DHT22)
datos.humedad = 40.0 + random(0, 400) / 10.0; // valor simulado
datos.contador++;
esp_err_t resultado = esp_now_send(macReceptor, (uint8_t *)&datos, sizeof(datos));
Serial.printf("Enviando msg #%lu -> %s\n", (unsigned long)datos.contador,
resultado == ESP_OK ? "enviado a la cola" : "error de envío");
}
}
codigo/nodo_muchos_a_muchos/nodo_muchos_a_muchos.ino// nodo_muchos_a_muchos.ino · Todos los nodos envían y reciben (broadcast)
// Sube EL MISMO programa a todas las placas cambiando solo ID_NODO.
#include <WiFi.h>
#include <esp_now.h>
#define ID_NODO 1 // Cambia en cada placa (1, 2, 3...)
// Dirección de difusión: la reciben todas las placas a la vez
uint8_t broadcastMAC[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
typedef struct {
uint8_t id;
float valor;
uint32_t contador;
} Mensaje;
Mensaje enviado, recibido;
unsigned long ultimoEnvio = 0;
void procesar(const uint8_t *mac, const uint8_t *entrada, int longitud) {
if (longitud != sizeof(Mensaje)) return;
memcpy(&recibido, entrada, sizeof(Mensaje));
Serial.printf("[Nodo %u] recibido de nodo %u (%02X:%02X:%02X:%02X:%02X:%02X): valor=%.1f msg #%lu\n",
ID_NODO, recibido.id, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
recibido.valor, (unsigned long)recibido.contador);
}
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
void alRecibir(const esp_now_recv_info_t *info, const uint8_t *entrada, int longitud) {
procesar(info->src_addr, entrada, longitud);
}
#else
void alRecibir(const uint8_t *mac, const uint8_t *entrada, int longitud) {
procesar(mac, entrada, longitud);
}
#endif
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
delay(100);
Serial.print("Nodo "); Serial.print(ID_NODO);
Serial.print(" listo. MAC: "); Serial.println(WiFi.macAddress());
if (esp_now_init() != ESP_OK) {
Serial.println("Error al iniciar ESP-NOW");
return;
}
esp_now_register_recv_cb(alRecibir);
esp_now_peer_info_t peer;
memset(&peer, 0, sizeof(peer));
memcpy(peer.peer_addr, broadcastMAC, 6);
peer.channel = 0;
peer.encrypt = false; // el broadcast no admite cifrado
esp_now_add_peer(&peer);
}
void loop() {
if (millis() - ultimoEnvio >= 3000) {
ultimoEnvio = millis();
enviado.id = ID_NODO;
enviado.valor = 20.0 + random(0, 100) / 10.0; // valor simulado
enviado.contador++;
esp_now_send(broadcastMAC, (uint8_t *)&enviado, sizeof(enviado));
}
}
#if ESP_ARDUINO_VERSION porque la firma de los callbacks cambió entre versiones del núcleo ESP32 (2.x, 3.0 y 3.3+). Los valores de temperatura y humedad son simulados; para datos reales, sustituirlos por lecturas del DHT22.Salida esperada del Monitor Serie
Receptor listo. Mi MAC: 24:6F:28:AA:BB:CC Emisor 1 (30:AE:A4:11:22:33) | T=23.4 C | H=57.2 % | msg #1 Emisor 2 (7C:9E:BD:44:55:66) | T=21.8 C | H=48.9 % | msg #1 Emisor 1 (30:AE:A4:11:22:33) | T=25.1 C | H=61.0 % | msg #2 Emisor 2 (7C:9E:BD:44:55:66) | T=22.3 C | H=50.4 % | msg #2
Emisor 1 listo. Mi MAC: 30:AE:A4:11:22:33 Enviando msg #1 -> enviado a la cola Estado del envío: ENTREGADO Enviando msg #2 -> enviado a la cola Estado del envío: ENTREGADO
Problemas frecuentes
Siempre sale “FALLO” en el envío
Comprueba que la MAC del receptor es correcta, que ambas placas están en el mismo canal y que el receptor está encendido y con ESP-NOW iniciado.
No compila: error en el callback
La firma de los callbacks depende de la versión del núcleo ESP32. Los sketches incluyen guardas, pero si tu versión es distinta, ajusta los parámetros al tipo que indique el error.
El receptor no recibe nada
Verifica que la estructura de datos es idéntica en emisor y receptor y que se llamó a WiFi.mode(WIFI_STA) antes de esp_now_init().
Si uso WiFi a la vez, se pierde la comunicación
El canal del router puede diferir del canal ESP-NOW. Ambos extremos deben usar el mismo canal.
Conclusión y fuentes
ESP-NOW permite comunicar placas ESP32 de forma directa, rápida y sin infraestructura. Con la topología muchos a 1 se monta una red de sensores simple y con muchos a muchos una malla de nodos iguales. Basta conocer la MAC de cada placa y compartir canal.