Cloud, DevOps e IoT en español

Parte 4 de 22 de la serie Domótica con ESP32 y AWS desde cero

ESP-NOW paso a paso: enviar órdenes entre ESP32 con confirmación

3 de octubre de 2026 · Steven Carvajal · Tutoriales

ESP-NOW es el protocolo de Espressif para que dos ESP32 se hablen directamente, sin router ni servidor. En el proyecto de ejemplo de esta serie es lo que permite tener un solo hub conectado a internet: recibe las órdenes de la nube y las reparte a los nodos que mueven relés (parte 1).

Un ESP32-S3 arriba y dos ESP32-C3 abajo, placas que se pueden comunicar por ESP-NOW

Un ESP32-S3 como hub (arriba) y dos ESP32-C3 como nodos (abajo): entre ellos no hace falta router.

Los ejemplos básicos envían un mensaje de una placa a otra. En esta parte vamos un paso más allá: un mensaje con estructura propia, una confirmación de que la orden se aplicó y qué hacer cuando no llega.

Qué es ESP-NOW

ESP-NOW envía paquetes pequeños (hasta 250 bytes en la versión clásica) directamente de una dirección MAC a otra, usando la radio Wi-Fi pero sin asociarse a ninguna red. No hay handshake TLS, ni DHCP, ni router de por medio.

Ventajas para domótica:

La contrapartida: no hay garantía de que el nodo aplicó la orden. La radio confirma que el paquete llegó a la otra antena, pero no que el programa lo procesó. Por eso conviene construir una confirmación propia encima.

El requisito que más falla: el mismo canal

ESP-NOW solo funciona si emisor y receptor están en el mismo canal de radio. Si el hub está conectado al Wi-Fi de la casa, su canal es el del router, que puede ser cualquiera del 1 al 13. Hay tres formas de resolverlo:

OpciónCómoInconveniente
Fijar el canalConfiguras el router en un canal fijo y lo pones como constante en los nodosSi alguien cambia el router, hay que reprogramar
EscanearEl nodo escanea las redes, encuentra la del hub por su nombre y usa su canal, sin conectarseEl nodo necesita saber el nombre de la red
Probar canalesEl nodo envía un mensaje en cada canal hasta que el hub respondeArranque más lento

Para empezar, lo más simple es la primera opción: fija el canal del router a mano (en automático puede cambiar solo) y usa ese número en los nodos.

El mensaje: una estructura con número de orden

En lugar de enviar texto, conviene definir una estructura que compartan el hub y los nodos:

// Mismo archivo en el hub y en los nodos
typedef struct __attribute__((packed)) {
  uint8_t  tipo;    // 1 = orden, 2 = confirmación
  uint8_t  salida;  // qué relé o luz del nodo
  uint8_t  valor;   // 0 = apagado, 1 = encendido
  uint16_t id;      // número de la orden, para emparejar la confirmación
} Mensaje;

El id permite saber a qué orden corresponde cada confirmación. El atributo packed evita que el compilador agregue relleno entre campos, así ambos lados leen los mismos bytes.

El código del hub

Para probar sin nube, las órdenes llegan por el Monitor Serie: escribe 1 para encender y 0 para apagar. En la serie, más adelante, llegarán del Device Shadow (parte 5).

// Hub ESP-NOW: envía órdenes a un nodo y espera su confirmación.
#include <WiFi.h>
#include <esp_now.h>

#define WIFI_SSID      "tu-red-wifi"
#define WIFI_PASSWORD  "tu-clave-wifi"
#define TIMEOUT_MS     2000

typedef struct __attribute__((packed)) {
  uint8_t tipo, salida, valor;
  uint16_t id;
} Mensaje;

uint8_t macNodo[6] = { 0x24, 0x0A, 0xC4, 0x00, 0x00, 0x01 };  // la imprime el nodo al arrancar

uint16_t siguienteId = 1;
uint16_t idPendiente = 0;        // 0 = no hay orden esperando confirmación
unsigned long enviadoEn = 0;

void enviarOrden(uint8_t valor) {
  Mensaje m = { 1, 1, valor, siguienteId++ };
  esp_now_send(macNodo, (uint8_t*)&m, sizeof(m));
  idPendiente = m.id;
  enviadoEn = millis();
  Serial.printf("-> orden #%u: salida 1 = %u\n", m.id, valor);
}

void alRecibir(const esp_now_recv_info_t* info, const uint8_t* data, int len) {
  if (len != sizeof(Mensaje) || memcmp(info->src_addr, macNodo, 6) != 0) return;  // solo nodos conocidos
  Mensaje m;
  memcpy(&m, data, sizeof(m));
  if (m.tipo == 2 && m.id == idPendiente) {
    Serial.printf("<- confirmación #%u: salida %u = %u (%lu ms)\n", m.id, m.salida, m.valor, millis() - enviadoEn);
    idPendiente = 0;
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(300);
  Serial.printf("Wi-Fi en el canal %d. MAC del hub: %s\n", WiFi.channel(), WiFi.macAddress().c_str());

  if (esp_now_init() != ESP_OK) { Serial.println("Error iniciando ESP-NOW"); return; }
  esp_now_register_recv_cb(alRecibir);

  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, macNodo, 6);
  peer.channel = 0;      // 0 = el canal actual del Wi-Fi
  peer.encrypt = false;
  esp_now_add_peer(&peer);
}

void loop() {
  if (Serial.available()) {
    char c = Serial.read();
    if (c == '0' || c == '1') enviarOrden(c - '0');
  }
  if (idPendiente && millis() - enviadoEn > TIMEOUT_MS) {
    Serial.printf("!  sin confirmación de la orden #%u\n", idPendiente);
    idPendiente = 0;  // no se asume que el cambio se aplicó
  }
}

El código del nodo

// Nodo ESP-NOW: aplica la orden y responde con una confirmación.
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>

#define CANAL    6   // el mismo canal fijo del router
#define LED_PIN  8   // LED del ESP32-C3 Super Mini (activo en bajo); aquí iría el relé

typedef struct __attribute__((packed)) {
  uint8_t tipo, salida, valor;
  uint16_t id;
} Mensaje;

uint8_t macHub[6] = { 0x24, 0x0A, 0xC4, 0x00, 0x00, 0x10 };  // la imprime el hub al arrancar

void alRecibir(const esp_now_recv_info_t* info, const uint8_t* data, int len) {
  if (len != sizeof(Mensaje) || memcmp(info->src_addr, macHub, 6) != 0) return;
  Mensaje m;
  memcpy(&m, data, sizeof(m));
  if (m.tipo != 1) return;

  digitalWrite(LED_PIN, m.valor ? LOW : HIGH);

  m.tipo = 2;  // misma orden, ahora como confirmación de lo que se aplicó
  esp_now_send(macHub, (uint8_t*)&m, sizeof(m));
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);  // apagado al arrancar

  WiFi.mode(WIFI_STA);
  esp_wifi_set_channel(CANAL, WIFI_SECOND_CHAN_NONE);
  Serial.printf("Canal %d. MAC del nodo: %s\n", CANAL, WiFi.macAddress().c_str());

  if (esp_now_init() != ESP_OK) { Serial.println("Error iniciando ESP-NOW"); return; }
  esp_now_register_recv_cb(alRecibir);

  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, macHub, 6);
  peer.channel = 0;
  peer.encrypt = false;
  esp_now_add_peer(&peer);
}

void loop() {
  delay(50);
}

Las firmas de los callbacks corresponden al core 3.x de ESP32 para Arduino. Si usas el core 2.x, el callback de recepción recibe const uint8_t* mac en lugar de esp_now_recv_info_t*.

Cómo probarlo

  1. Carga el nodo y copia la MAC que imprime en el Monitor Serie.
  2. Ponla en macNodo del hub, junto con tu red Wi-Fi, y carga el hub. Comprueba que el canal que imprime coincide con CANAL.
  3. Copia la MAC del hub en macHub del nodo y vuelve a cargarlo.
  4. En el Monitor Serie del hub escribe 1: la orden sale y la confirmación vuelve en milisegundos.
  5. Desconecta el nodo y escribe 0: a los 2 segundos verás el aviso de que no hubo confirmación.

Monitor Serie del hub ESP-NOW: órdenes enviadas, confirmaciones con su tiempo en milisegundos y una orden sin confirmación cuando el nodo está desconectado

Salida esperada en el Monitor Serie del hub. En verde, las confirmaciones; en amarillo, una orden sin respuesta. Las MAC son de ejemplo.

Buenas prácticas para un sistema real

Límites de ESP-NOW

Si algo no funciona, revisa por qué ESP-NOW no funciona: causas y soluciones. Y para comparar ESP-NOW con Wi-Fi, BLE o Zigbee, está la guía de comunicación entre dispositivos.

El material

ESP32-S3 Super Mini

Para el hub: dos núcleos, USB-C y LED RGB integrado.

Enlace de afiliado: si compras, el sitio recibe una comisión sin costo extra para ti.

ESP32-C3 Super Mini

Para cada nodo: pequeña, barata y con ESP-NOW.

Enlace de afiliado: si compras, el sitio recibe una comisión sin costo extra para ti.

Preguntas frecuentes

¿ESP-NOW funciona si el hub está conectado al Wi-Fi?

Sí. El hub puede estar conectado al router y usar ESP-NOW a la vez, siempre que los nodos estén en el mismo canal que el router.

¿Qué alcance tiene?

El mismo que el Wi-Fi de las placas, porque usa la misma radio. En una casa normal cubre varias habitaciones; las paredes gruesas y la antena de cada placa lo reducen.

¿Puedo tener más de 20 nodos?

No registrados a la vez en un solo hub. Las salidas son un segundo hub, o enviar en broadcast y filtrar por un identificador dentro del mensaje.

¿Necesito el id en el mensaje?

Si solo envías una orden a la vez, no es imprescindible. Con varias órdenes seguidas, sin él no sabes qué confirmación corresponde a cuál.

Sigue leyendo