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.

Iván Palomo RodríguezFP Electricidad-ElectrónicaUnicarlos
ESP32 ESP32 ESP32 ESP32 sin router
01

Cumplimiento de la tarea

Resumen de lo que pide la práctica y dónde está resuelto en esta página.

RequisitoDóndeContenido
Qué es ESP-NOWSección 02Protocolo propietario de Espressif, sin router✔
Cómo funcionaSección 03Tramas de acción, peers, ACK, callbacks + esquema SVG✔
Para qué sirveSección 07Casos de uso y comparativa (04, 06)✔
Sin router / MAC / latenciaSección 02 · 04Identificación por MAC, latencia baja✔
Topología muchos a 1 y muchos a muchosSección 05Ambas explicadas con diagrama SVG✔
Esquema / diagrama SVGSección 03 · 05Secuencia, estructura de trama y 4 topologías✔
Código .inoSección 084 sketches en la carpeta codigo/✔
Capturas Monitor SerieSección 09Salida de ejemplo (no es captura real)✔
02

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).

0 APSin routerComunicación directa placa a placa
250 BPayloadHasta 250 bytes por mensaje (ESP-NOW v1.0)
20PeersMáx. 20 registrados, 6 cifrados
2.4 GHzBanda2,4 GHz, mismo hardware que WiFi
  • 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.
03

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.

Emisor (ESP32) Receptor (ESP32) 1. esp_now_init() + add_peer(MAC receptor) 2. esp_now_send(): trama de acción (≤250 B) 3. ACK de capa de enlace (automático) 4. Callback de envío: ENTREGADO / FALLO 5. Callback de recepción: (MAC origen, datos, longitud)
Secuencia de un envío ESP-NOW: sin asociación previa ni router, solo emisor y receptor.
  1. Inicializar: WiFi.mode(WIFI_STA) y esp_now_init().
  2. Registrar peers: con esp_now_add_peer() se da de alta la MAC de cada destino (canal y cifrado opcional).
  3. Enviar: esp_now_send(mac, datos, longitud). El resultado llega al callback de envío.
  4. Recibir: esp_now_register_recv_cb() llama a tu función con la MAC de origen y los datos.

Estructura de la trama

MAC headerDir. destino / origen
Categorycódigo de acción
Org. ID0x18FE34 (Espressif)
Randomvalores aleatorios
Vendor contentdatos de usuario ≤ 250 B
FCScomprobación
Importante: emisor y receptor deben usar el mismo canal WiFi. Con channel = 0 se usa el canal actual de la placa (por defecto el 1). El cifrado opcional (CCMP) admite hasta 6 peers cifrados.
04

Características principales

CaracterísticaValor
Banda2,4 GHz (IEEE 802.11)
Tamaño de mensajeHasta 250 bytes (v1.0); versiones nuevas de ESP-IDF admiten más
PeersHasta 20 en total (6 con cifrado)
ModosUnicast (con ACK) y broadcast (sin ACK)
CifradoOpcional, CCMP con claves PMK/LMK de 16 bytes
DireccionamientoDirección MAC de 6 bytes
ConexiónSin conexión: no hay handshake ni asociación
LatenciaMuy baja (orientativo: milisegundos), sin negociar red
AlcanceDecenas de metros en interior; más en campo abierto (orientativo, depende de antena y entorno)
CoexistenciaPuede usarse a la vez que WiFi STA si comparten canal
Los valores de latencia y alcance son orientativos: varían con el entorno, la antena y la carga de la red 2,4 GHz.
05

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.

EmisorMAC AReceptorMAC Bdatos →← ACK
Uno a uno: el emisor conoce la MAC del receptor y recibe confirmación (ACK).

Un emisor y un receptor. Es la base: se registra la MAC del otro como peer y se llama a esp_now_send().

EmisorbroadcastNodo 1MAC 1Nodo 2MAC 2Nodo 3MAC 3
Uno a muchos: un emisor envía a varios receptores (broadcast FF:FF:FF:FF:FF:FF o varias MAC).

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).

Emisor 1ID 1Emisor 2ID 2Emisor 3ID 3ReceptorMAC única
Muchos a 1: todos los emisores apuntan a la MAC del receptor, que identifica a cada uno por su MAC / campo id.

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.

Nodo AMAC ANodo BMAC BNodo CMAC CNodo DMAC D
Muchos a muchos: todos los nodos envían y reciben (cada uno registra a los demás como peers o usa broadcast).

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.

06

Comparativa

ESP-NOWWiFi con routerBluetooth LE
Necesita routerNoSíNo (emparejamiento)
DirecciónMACIPMAC / GATT
Establecer conexiónNoSí (asociación + DHCP)Sí (conexión)
LatenciaMuy bajaMediaBaja
Tamaño de dato≤ 250 BGrande (TCP/HTTP)Pequeño (~20-512 B)
Acceso a InternetNo directoSíNo
Mejor usoSensores locales y controlServicios web / nubeMóvil ↔ dispositivo
07

Para qué sirve

🌡Red de sensoresVarios nodos con DHT22 envían a un receptor central
🏠DomóticaPulsadores y relés que se hablan sin router
🌱Invernaderos / campoSin WiFi disponible, bajo consumo
🎮Mandos y robóticaLatencia mínima para control remoto

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.

08

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.

  1. Sube mac_address.ino a la placa receptora y anota su MAC.
  2. Pega esa MAC en macReceptor[] de emisor_muchos_a_1.ino.
  3. Sube el emisor a cada placa cambiando ID_EMISOR (1, 2, 3…).
  4. Sube receptor_muchos_a_1.ino a la receptora y abre el Monitor Serie a 115200 baudios.
Obtener la MAC · 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() {
}
Receptor (muchos a 1) · 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()
}
Emisor (muchos a 1) · 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");
  }
}
Nodo (muchos a muchos) · 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));
  }
}
Los sketches usan directivas #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.
09

Salida esperada del Monitor Serie

Aviso: lo siguiente es un ejemplo de salida esperada, no una captura real. Las MAC son ficticias. Sustitúyelo por tus capturas si pruebas el montaje en clase.
Receptor · Monitor Serie (115200)
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 · Monitor Serie (115200)
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
10

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.

11

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.