MA
Mario Alcantud Bleda
Cambiar Tema
Mario Alcantud Bleda • 2º ME (Mantenimiento Electrónico)

ESP-NOW

Comunicación inalámbrica punto a punto y multipunto de ultra baja latencia sin router entre microcontroladores ESP32 con control de LEDs mediante pulsadores.

Enunciado de la Tarea

Documentación técnica completa del protocolo ESP-NOW (protocolo propietario desarrollado por Espressif Systems que opera sobre la capa MAC en la banda de 2.4 GHz sin necesidad de punto de acceso Wi-Fi ni enrutador), análisis de sus topologías de conexión y el desarrollo práctico de un transceptor con pulsadores y diodos LED indicadores.

Ver enunciado oficial en Unicarlos

¿Qué es ESP-NOW y Cómo Funciona?

ESP-NOW es un protocolo de red inalámbrica optimizado por Espressif para comunicaciones locales de alta velocidad, respuesta inmediata y mínimo consumo energético. A diferencia de la pila TCP/IP tradicional:

Sin Router / Peer-to-Peer Las placas se comunican directamente por radio en la capa MAC sin requerir router Wi-Fi, negociación DHCP ni contraseñas WPA.
Direccionamiento MAC Cada nodo se identifica y empareja mediante su dirección física MAC de 6 bytes (p. ej. 30:AE:A4:07:0F:70) para envíos Unicast o Broadcast.
Latencia Mínima (< 5 ms) Al prescindir del handshake TCP/IP y capas superiores, la transmisión de cargas útiles de hasta 250 bytes se realiza casi instantáneamente.
Optimizado para Deep Sleep Permite que un nodo sensor despierte de reposo profundo, inicialice la radio, envíe su paquete y se vuelva a dormir en menos de 10 milisegundos.

Topologías Soportadas por ESP-NOW

La flexibilidad de ESP-NOW permite crear arquitecturas adaptadas a cualquier proyecto de electrónica o domótica:

ESP32 EMISOR (TX) Modo WIFI_STA Pulsador 1 (Pin GPIO 4) Pulsador 2 (Pin GPIO 5) ESP-NOW 2.4 GHz Directo ESP32 RECEPTOR (RX) Control de Salidas Pin GPIO 14: LED Verde (Fijo) Pin GPIO 12: LED Amarillo (Blink) Pin GPIO 13: LED Rojo (Fijo)

Montaje Real en Protoboard y Comportamiento

Fotografías del Montaje Real en Protoboard

Montaje del estado con Pulsador Izquierdo activo
Pulsador 1 Activo (Izquierda): Al presionar el botón izquierdo en la placa emisora TX, la pantalla OLED refleja IZQUIERDA y el receptor RX enciende el LED Verde (fijo) e inicia el parpadeo del LED Amarillo.
Montaje del estado con Pulsador Derecho activo
Pulsador 2 Activo (Derecha): Al pulsar el botón derecho, la pantalla OLED actualiza a DERECHA y el receptor conmuta apagando Verde/Amarillo y encendiendo únicamente el LED Rojo fijo.

Demostración en Vídeo del Funcionamiento

Vídeo en vivo: Prueba práctica alternando pulsadores en el protoboard y comprobando la transmisión inalámbrica inmediata en las salidas luminosas y pantallas OLED.

Tabla de Respuesta de los LEDs en el Receptor

Acción en el Emisor (TX)LED Verde (GPIO 14)LED Amarillo (GPIO 12)LED Rojo (GPIO 13)
Pulsador 1 (Izquierda) presionado ENCENDIDO FIJO PARPADEANDO (BLINK 180ms) APAGADO
Pulsador 2 (Derecha) presionado APAGADO APAGADO ENCENDIDO FIJO
Reposo / Pérdida de Señal (> 700ms) APAGADOAPAGADOAPAGADO

Distribución de Pines y Componentes

Placa ESP32Componente ConectadoPin GPIOConfiguración y Modo de Operación
ESP32 EmisorPulsador 1 (Izquierda)GPIO 4INPUT_PULLUP (Activo a nivel bajo GND al pulsar)
ESP32 EmisorPulsador 2 (Derecha)GPIO 5INPUT_PULLUP (Activo a nivel bajo GND al pulsar)
ESP32 Emisor / ReceptorPantalla OLED SH1106 I2CGPIO 21 (SDA) / 22 (SCL)I2C a 100 kHz (Dirección 0x3C)
ESP32 ReceptorDiodo LED VerdeGPIO 14OUTPUT (Nivel alto 3.3V con comando 1)
ESP32 ReceptorDiodo LED AmarilloGPIO 12OUTPUT (Conmutación con millis() en comando 1)
ESP32 ReceptorDiodo LED RojoGPIO 13OUTPUT (Nivel alto 3.3V con comando 2)

Corrección del Error de Compilación en Arduino IDE 2.3+ / ESP32 Core v3.x

Al actualizar el paquete de tarjetas ESP32 en Arduino IDE a las versiones Core 3.x (basadas en ESP-IDF v5.x), el compilador genera un error de conversión en los callbacks de envío y recepción:

Firma obsoleta (Core ESP32 v2.x): void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len)
Nueva firma (Core ESP32 v3.x / IDE 2.3+): void OnDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len)

Descarga de Fuentes del Proyecto

Descarga los bocetos de código fuente listos para compilar en Arduino IDE:

Descargar esp_now_emisor.ino Descargar esp_now_receptor.ino

Código 1: ESP32 Emisor (Pulsadores y OLED)

El emisor configura los GPIO 4 y 5 en modo INPUT_PULLUP, gestiona la interfaz gráfica en la pantalla OLED SH1106 mediante I2C y transmite paquetes continuos por ESP-NOW al receptor:

esp_now_emisor.ino
Arduino C++ Descargar
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <cstring>

// ---------------- CONFIGURACION ----------------
#define CANAL_WIFI 1

#define BTN_IZQ 4
#define BTN_DER 5

#define OLED_ANCHO 128
#define OLED_ALTO 64
#define OLED_RESET -1
#define OLED_DIR 0x3C

Adafruit_SH1106G display(OLED_ANCHO, OLED_ALTO, &Wire, OLED_RESET);

uint8_t macReceptor[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

typedef struct {
  uint8_t comando;
} Mensaje;

Mensaje datosEnvio;
unsigned long ultimoEnvio = 0;
uint8_t comandoAnterior = 255;
bool oledDisponible = false;

// ---------------- TEXTO CENTRADO ----------------
void textoCentrado(const char* texto, int y, int tamano) {
  display.setTextSize(tamano);
  int ancho = strlen(texto) * 6 * tamano;
  int x = (OLED_ANCHO - ancho) / 2;
  display.setCursor(x, y);
  display.print(texto);
}

// ---------------- CONFIGURACION OLED ----------------
void iniciarOLED() {
  Wire.begin(21, 22);
  Wire.setClock(100000);

  oledDisponible = display.begin(OLED_DIR, true);

  if (!oledDisponible) {
    Serial.println("ERROR: NO SE DETECTA LA OLED");
    return;
  }

  display.clearDisplay();
  display.display();
  delay(50);

  display.setRotation(0);
  display.setTextColor(SH110X_WHITE, SH110X_BLACK);
  display.setTextWrap(false);

  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);
  textoCentrado("INICIANDO", 19, 2);
  textoCentrado("ESP32 EMISOR", 43, 1);
  display.display();
  delay(1200);
}

// ---------------- ACTUALIZAR PANTALLA ----------------
void actualizarPantalla(uint8_t comando) {
  if (!oledDisponible) return;

  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);

  display.setTextSize(1);
  display.setCursor(7, 6);
  display.print(F("CONTROL ESP-NOW"));
  display.setCursor(105, 6);
  display.print(F("TX"));
  display.drawLine(5, 18, 122, 18, SH110X_WHITE);

  if (comando == 1) {
    textoCentrado("PULSADOR IZQ.", 26, 1);
    textoCentrado("IZQUIERDA", 39, 2);
  } else if (comando == 2) {
    textoCentrado("PULSADOR DER.", 26, 1);
    textoCentrado("DERECHA", 39, 2);
  } else {
    textoCentrado("SISTEMA ACTIVO", 26, 1);
    textoCentrado("ESPERANDO", 39, 2);
  }

  display.drawLine(5, 55, 122, 55, SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(26, 57);
  display.print(F("TRANSMISOR"));
  display.display();
}

// ---------------- SETUP ----------------
void setup() {
  Serial.begin(115200);

  pinMode(BTN_IZQ, INPUT_PULLUP);
  pinMode(BTN_DER, INPUT_PULLUP);

  iniciarOLED();

  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  Serial.println("\n=== EMISOR ESP-NOW ===");
  Serial.print("MAC: ");
  Serial.println(WiFi.macAddress());

  if (esp_wifi_set_channel(CANAL_WIFI, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
    Serial.println("ERROR CONFIGURANDO CANAL");
    while (true) delay(1000);
  }

  if (esp_now_init() != ESP_OK) {
    Serial.println("ERROR INICIANDO ESP-NOW");
    while (true) delay(1000);
  }

  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, macReceptor, 6);
  peerInfo.channel = CANAL_WIFI;
  peerInfo.encrypt = false;
  peerInfo.ifidx = WIFI_IF_STA;

  if (!esp_now_is_peer_exist(macReceptor)) {
    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
      Serial.println("ERROR REGISTRANDO RECEPTOR");
      while (true) delay(1000);
    }
  }

  Serial.println("EMISOR PREPARADO");
  actualizarPantalla(0);
}

// ---------------- LOOP ----------------
void loop() {
  bool izquierda = digitalRead(BTN_IZQ) == LOW;
  bool derecha = digitalRead(BTN_DER) == LOW;
  uint8_t comando = 0;

  if (izquierda) comando = 1;
  else if (derecha) comando = 2;

  datosEnvio.comando = comando;

  if (millis() - ultimoEnvio >= 100) {
    esp_err_t resultado = esp_now_send(macReceptor, (uint8_t*)&datosEnvio, sizeof(datosEnvio));
    if (resultado != ESP_OK) {
      Serial.print("ERROR ENVIO: ");
      Serial.println(esp_err_to_name(resultado));
    }
    ultimoEnvio = millis();
  }

  if (comando != comandoAnterior) {
    actualizarPantalla(comando);
    comandoAnterior = comando;
  }

  delay(10);
}

Código 2: ESP32 Receptor (LEDs y OLED)

El receptor captura las tramas mediante la rutina de interrupción de ESP-NOW de forma segura con exclusión mutua (portMUX_TYPE), controla los 3 LEDs indicadores sin bloqueos mediante millis() y gestiona el Timeout por desconexión:

esp_now_receptor.ino
Arduino C++ Descargar
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <cstring>

// ---------------- CONFIGURACION ----------------
#define CANAL_WIFI 1

#define LED_VERDE 14
#define LED_AMARILLO 12
#define LED_ROJO 13

#define OLED_ANCHO 128
#define OLED_ALTO 64
#define OLED_RESET -1
#define OLED_DIR 0x3C

#define TIEMPO_ESPERA 700

Adafruit_SH1106G display(OLED_ANCHO, OLED_ALTO, &Wire, OLED_RESET);

typedef struct {
  uint8_t comando;
} Mensaje;

// Comunicacion
volatile uint8_t comandoActual = 0;
volatile unsigned long ultimoMensaje = 0;
volatile bool mensajeRecibido = false;

portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

bool oledDisponible = false;
uint8_t comandoAnteriorPantalla = 255;
bool senalAnterior = false;

uint8_t comandoAnteriorLED = 255;
unsigned long tiempoBlink = 0;
bool estadoAmarillo = false;

// ---------------- TEXTO CENTRADO ----------------
void textoCentrado(const char* texto, int y, int tamano) {
  display.setTextSize(tamano);
  int ancho = strlen(texto) * 6 * tamano;
  int x = (OLED_ANCHO - ancho) / 2;
  display.setCursor(x, y);
  display.print(texto);
}

// ---------------- CONFIGURACION OLED ----------------
void iniciarOLED() {
  Wire.begin(21, 22);
  Wire.setClock(100000);

  oledDisponible = display.begin(OLED_DIR, true);

  if (!oledDisponible) {
    Serial.println("ERROR: NO SE DETECTA LA OLED");
    return;
  }

  display.clearDisplay();
  display.display();
  delay(50);

  display.setRotation(0);
  display.setTextColor(SH110X_WHITE, SH110X_BLACK);
  display.setTextWrap(false);

  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);
  textoCentrado("INICIANDO", 19, 2);
  textoCentrado("ESP32 RECEPTOR", 43, 1);
  display.display();
  delay(1200);
}

// ---------------- ACTUALIZAR PANTALLA ----------------
void actualizarPantalla(uint8_t comando, bool senal) {
  if (!oledDisponible) return;

  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);

  display.setTextSize(1);
  display.setCursor(7, 6);
  display.print(F("CONTROL ESP-NOW"));
  display.setCursor(105, 6);
  display.print(F("RX"));
  display.drawLine(5, 18, 122, 18, SH110X_WHITE);

  if (!senal) {
    textoCentrado("SIN CONEXION", 26, 1);
    textoCentrado("ESPERANDO", 39, 2);
  } else if (comando == 1) {
    textoCentrado("SENAL RECIBIDA", 26, 1);
    textoCentrado("IZQUIERDA", 39, 2);
  } else if (comando == 2) {
    textoCentrado("SENAL RECIBIDA", 26, 1);
    textoCentrado("DERECHA", 39, 2);
  } else {
    textoCentrado("SENAL RECIBIDA", 26, 1);
    textoCentrado("REPOSO", 39, 2);
  }

  display.drawLine(5, 55, 122, 55, SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(31, 57);
  display.print(F("RECEPTOR"));
  display.display();
}

// ---------------- RECEPCION ESP-NOW ----------------
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *datos, int longitud) {
  if (longitud != sizeof(Mensaje)) return;

  Mensaje recibido;
  memcpy(&recibido, datos, sizeof(Mensaje));

  if (recibido.comando > 2) return;

  portENTER_CRITICAL(&mux);
  comandoActual = recibido.comando;
  ultimoMensaje = millis();
  mensajeRecibido = true;
  portEXIT_CRITICAL(&mux);
}

// ---------------- SETUP ----------------
void setup() {
  Serial.begin(115200);

  pinMode(LED_VERDE, OUTPUT);
  pinMode(LED_AMARILLO, OUTPUT);
  pinMode(LED_ROJO, OUTPUT);

  digitalWrite(LED_VERDE, LOW);
  digitalWrite(LED_AMARILLO, LOW);
  digitalWrite(LED_ROJO, LOW);

  iniciarOLED();

  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  Serial.println("\n=== RECEPTOR ESP-NOW ===");
  Serial.print("MAC: ");
  Serial.println(WiFi.macAddress());

  if (esp_wifi_set_channel(CANAL_WIFI, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
    Serial.println("ERROR CONFIGURANDO CANAL");
    while (true) delay(1000);
  }

  if (esp_now_init() != ESP_OK) {
    Serial.println("ERROR INICIANDO ESP-NOW");
    while (true) delay(1000);
  }

  if (esp_now_register_recv_cb(OnDataRecv) != ESP_OK) {
    Serial.println("ERROR RECEPCION");
    while (true) delay(1000);
  }

  Serial.println("RECEPTOR PREPARADO");
  actualizarPantalla(0, false);
}

// ---------------- LOOP ----------------
void loop() {
  uint8_t comando;
  unsigned long ultimo;
  bool recibido;

  portENTER_CRITICAL(&mux);
  comando = comandoActual;
  ultimo = ultimoMensaje;
  recibido = mensajeRecibido;
  portEXIT_CRITICAL(&mux);

  bool senal = recibido && (millis() - ultimo <= TIEMPO_ESPERA);
  if (!senal) comando = 0;

  if (comando != comandoAnteriorLED) {
    estadoAmarillo = false;
    tiempoBlink = millis();
    comandoAnteriorLED = comando;
  }

  // ---------------- CONTROL DE LED ----------------
  switch (comando) {
    case 1:
      digitalWrite(LED_VERDE, HIGH);
      digitalWrite(LED_ROJO, LOW);

      if (millis() - tiempoBlink >= 180) {
        tiempoBlink = millis();
        estadoAmarillo = !estadoAmarillo;
        digitalWrite(LED_AMARILLO, estadoAmarillo ? HIGH : LOW);
      }
      break;

    case 2:
      digitalWrite(LED_VERDE, LOW);
      digitalWrite(LED_AMARILLO, LOW);
      digitalWrite(LED_ROJO, HIGH);
      estadoAmarillo = false;
      break;

    default:
      digitalWrite(LED_VERDE, LOW);
      digitalWrite(LED_AMARILLO, LOW);
      digitalWrite(LED_ROJO, LOW);
      estadoAmarillo = false;
      break;
  }

  if (comando != comandoAnteriorPantalla || senal != senalAnterior) {
    actualizarPantalla(comando, senal);
    comandoAnteriorPantalla = comando;
    senalAnterior = senal;
  }

  delay(5);
}
Código copiado al portapapeles