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.
¿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:
30:AE:A4:07:0F:70) para envíos Unicast o Broadcast.
Topologías Soportadas por ESP-NOW
La flexibilidad de ESP-NOW permite crear arquitecturas adaptadas a cualquier proyecto de electrónica o domótica:
- 1 a 1 (Punto a Punto): Comunicación directa entre un mando emisor y un receptor.
- Muchos a 1 (Concentrador): Diversos nodos sensores repartidos envían mediciones a una estación central.
- 1 a Muchos (Difusión): Un controlador maestro envía comandos de acción o sincronización a múltiples actuadores.
- Muchos a Muchos (Malla): Todos los módulos ESP32 intercambian información bidireccional entre sí.
Montaje Real en Protoboard y Comportamiento
Fotografías del Montaje Real en Protoboard
IZQUIERDA y el receptor RX enciende el LED Verde (fijo) e inicia el parpadeo del LED Amarillo.
DERECHA y el receptor conmuta apagando Verde/Amarillo y encendiendo únicamente el LED Rojo fijo.
Demostración en Vídeo del Funcionamiento
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) | APAGADO | APAGADO | APAGADO |
Distribución de Pines y Componentes
| Placa ESP32 | Componente Conectado | Pin GPIO | Configuración y Modo de Operación |
|---|---|---|---|
| ESP32 Emisor | Pulsador 1 (Izquierda) | GPIO 4 | INPUT_PULLUP (Activo a nivel bajo GND al pulsar) |
| ESP32 Emisor | Pulsador 2 (Derecha) | GPIO 5 | INPUT_PULLUP (Activo a nivel bajo GND al pulsar) |
| ESP32 Emisor / Receptor | Pantalla OLED SH1106 I2C | GPIO 21 (SDA) / 22 (SCL) | I2C a 100 kHz (Dirección 0x3C) |
| ESP32 Receptor | Diodo LED Verde | GPIO 14 | OUTPUT (Nivel alto 3.3V con comando 1) |
| ESP32 Receptor | Diodo LED Amarillo | GPIO 12 | OUTPUT (Conmutación con millis() en comando 1) |
| ESP32 Receptor | Diodo LED Rojo | GPIO 13 | OUTPUT (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:
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len)
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:
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:
#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:
#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);
}