Tienes una red ESP NOW funcionando: cinco nodos con sensores en el patio, un receptor en la casa, todo afinado. Un día un nodo se moja, se quema el regulador y la placa queda inservible. Cambiarla por otra ESP32 es fácil; lo molesto viene después. Cada nodo de la red tiene registrada a la placa muerta por su dirección MAC, así que la nueva llega como una desconocida y te toca reprogramar todos los demás nodos con la dirección nueva.

Hay un atajo: hacer que la placa de reemplazo se presente con la misma MAC que tenía la dañada. En esta guía vas a aprender a leer todas las direcciones MAC de un ESP32 con ESP IDF (la de fábrica, la de WiFi en modo estación, la del punto de acceso y la de Bluetooth), a fijar una MAC personalizada desde el firmware y a usar eso para clonar un nodo sin tocar el resto de la red. Al final vas a entender también por qué no cualquier dirección sirve y en qué momento del arranque hay que fijarla para que el cambio tenga efecto.

La idea: por qué la MAC es la identidad de un nodo ESP NOW

Una dirección MAC (Media Access Control) son seis bytes que identifican a un equipo dentro de una red local. Se escribe como seis pares hexadecimales separados por dos puntos, por ejemplo 30:AE:A4:07:0D:64. Los tres primeros bytes suelen identificar al fabricante (Espressif tiene varios bloques asignados) y los tres últimos son el número de serie de cada chip.

En WiFi tradicional la MAC casi no se ve: el router la usa para asignar IP y listo. En ESP NOW, en cambio, es todo. Cuando un nodo quiere mandarle datos a otro, lo registra con esp_now_add_peer() pasándole la MAC del destino, y los mensajes que llegan se identifican por la MAC del remitente. Si cifras la comunicación, la clave local (LMK) también queda asociada a esa dirección. O sea: para la red, la MAC es el nodo.

Una MAC base y varias derivadas

El ESP32 no trae cuatro direcciones grabadas por separado. En el eFuse viene una sola MAC base de fábrica, y ESP IDF calcula las demás sumándole un número al último byte. En un ESP32 o un ESP32-S3 con la configuración por defecto queda así:

  • WiFi estación (STA): la base, tal cual.
  • WiFi punto de acceso (SoftAP): la base + 1.
  • Bluetooth: la base + 2.

Ese detalle es clave para clonar: si cambias la MAC base, todas las derivadas se mueven con ella. Por eso basta con fijar una sola dirección para que la placa nueva tenga exactamente las mismas MAC de WiFi y Bluetooth que tenía la anterior.

Las dos funciones que vas a usar

Ambas viven en esp_mac.h:

  • esp_read_mac() lee la dirección del tipo que le pidas: la base de fábrica, la de estación, la de SoftAP o la de Bluetooth.
  • esp_base_mac_addr_set() reemplaza la MAC base por una que tú le entregas.

Un punto que conviene tener claro desde el principio: el cambio no se graba en el chip. La MAC de fábrica sigue intacta en el eFuse y la personalizada vive solo en memoria mientras el programa corre. Si el firmware no vuelve a fijarla en el siguiente arranque, la placa vuelve a su dirección original. En la práctica eso significa que la llamada tiene que estar en el código que corre en cada arranque, y es una ventaja: no hay forma de "romper" la identidad de la placa de manera permanente con este método.

Hardware y software que necesitas

El montaje no lleva cables ni sensores: solo la placa conectada al computador.

  • Una placa ESP32 de cualquier familia. El tutorial original usa una ESP32-S3 programada por su USB nativo (USB JTAG integrado), pero un ESP32 DevKit clásico con conversor USB serial funciona igual.
  • Un cable USB de datos (no uno de solo carga, que es la causa número uno de "no aparece el puerto COM").
  • Visual Studio Code con la extensión oficial ESP IDF de Espressif ya instalada. Si todavía no la tienes, sigue primero la guía de inicio de ESP IDF en VS Code (enlazada en Recursos).

Para el caso de clonar un nodo vas a necesitar además la MAC de la placa dañada. Si la placa todavía enciende, la lees con el primer programa de esta guía. Si murió del todo, la tienes que sacar de otro lado: del código de los otros nodos (donde está escrita en el arreglo del peer), de la lista de clientes del router o de una etiqueta. De ahí sale el mejor consejo de toda esta guía: cuando instales un nodo, anota su MAC y pégala en la caja.

Preparar el proyecto en VS Code

La extensión ESP IDF crea un proyecto vacío con toda la estructura de carpetas y la configuración de CMake lista. Abre la extensión desde la barra lateral, despliega el menú "Advanced", entra a "New Project Wizard" y elige la versión del framework (el original usa la rama 6.x).

Asistente de nuevo proyecto de la extensión ESP IDF en VS Code, con la selección de versión del framework

En la lista de ejemplos, elige "ESP IDF Templates" y presiona "Create project using template sample project".

Selección de la plantilla de ejemplo ESP IDF para crear un proyecto nuevo

Se abre un formulario con cinco datos:

  • Project Name: el nombre que quieras, por ejemplo mac-esp32.
  • Project Directory: la carpeta donde se guarda el proyecto. Evita carpetas sincronizadas con Google Drive, OneDrive o Dropbox: la compilación genera cientos de archivos y la sincronización la vuelve lentísima.
  • ESP IDF Target: el chip de tu placa (esp32, esp32s3, esp32c3, etc.). Con una ESP32-S3 por USB nativo, en "ESP IDF Board" se elige la configuración "ESP32-S chip (via builtin USB JTAG)".
  • Serial Port: con la placa conectada, el puerto que le corresponde.
  • Choose Template: el botón azul que crea el proyecto.

Formulario del asistente de proyecto ESP IDF con nombre, carpeta, chip, puerto y plantilla

A los pocos segundos aparece un aviso para abrir el proyecto recién creado; haz clic en "Open Project".

Aviso de VS Code para abrir el proyecto de ejemplo ESP IDF recién creado

Si el aviso no salió, abre la carpeta a mano desde File > Open Folder y selecciona la carpeta del proyecto.

Diálogo para seleccionar la carpeta del proyecto ESP IDF en VS Code

En el explorador de la izquierda, despliega la carpeta main y abre main.c. Ahí va todo el código de esta guía. (En el artículo original se cuela una mención a un archivo scan.c que viene de otro tutorial de los mismos autores; en la plantilla vacía el archivo es main.c).

Paso 1: leer todas las MAC de la placa

Este primer programa no cambia nada: solo consulta las cuatro direcciones y las imprime en el monitor serie. Úsalo para conocer la identidad de cada placa antes de instalarla, o para rescatar la MAC de un nodo que todavía enciende.

C
/*  
  Rui Santos & Sara Santos - Random Nerd Tutorials
  https://RandomNerdTutorials.com/esp-idf-esp32-mac-address/
*/
#include <stdio.h>
#include "esp_mac.h"
#include "esp_log.h"

static const char *TAG = "MAC";

void app_main(void)
{
    uint8_t mac[6];
    esp_err_t ret;

    // Base MAC Address (factory default)
    ret = esp_read_mac(mac, ESP_MAC_EFUSE_FACTORY);
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "Base MAC Address (Factory): %02X:%02X:%02X:%02X:%02X:%02X",
                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    // Wi-Fi Station MAC Address
    ret = esp_read_mac(mac, ESP_MAC_WIFI_STA);
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "Wi-Fi STA MAC Address: %02X:%02X:%02X:%02X:%02X:%02X",
                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    // Wi-Fi SoftAP MAC Address
    ret = esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "Wi-Fi SoftAP MAC Address: %02X:%02X:%02X:%02X:%02X:%02X",
                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    // Bluetooth MAC Address
    ret = esp_read_mac(mac, ESP_MAC_BT);
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "Bluetooth MAC Address: %02X:%02X:%02X:%02X:%02X:%02X",
                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }
}

Qué hace cada parte

Las tres cabeceras cumplen roles distintos: stdio.h es la biblioteca estándar de C, esp_mac.h trae las funciones de lectura y escritura de MAC, y esp_log.h entrega el sistema de logs de ESP IDF, que antepone a cada línea un nivel (I de info, E de error), una marca de tiempo y una etiqueta. Esa etiqueta es la constante TAG, que aquí vale "MAC" y te permite filtrar los mensajes de este módulo cuando el proyecto crezca.

El corazón del programa son cuatro llamadas casi iguales a esp_read_mac(). El primer parámetro es el arreglo de seis bytes donde se deja el resultado y el segundo dice qué dirección quieres:

  • ESP_MAC_EFUSE_FACTORY: la MAC base grabada de fábrica. Esta no cambia aunque fijes una personalizada, así que siempre te dice qué chip tienes en la mano.
  • ESP_MAC_WIFI_STA: la que usa el WiFi en modo estación, que es la que ven los otros nodos ESP NOW cuando trabajas en ese modo (el caso más común).
  • ESP_MAC_WIFI_SOFTAP: la del punto de acceso.
  • ESP_MAC_BT: la de Bluetooth.

Cada llamada devuelve un esp_err_t, y el programa solo imprime si el resultado es ESP_OK. El formato %02X muestra cada byte como dos dígitos hexadecimales en mayúscula, con cero a la izquierda cuando hace falta.

Compilar y grabar el programa

El flujo en VS Code es siempre el mismo y todo está en la barra inferior: método de grabación, puerto, chip, compilar y grabar. Si creaste el proyecto con el asistente, lo más probable es que ya esté todo configurado; igual revísalo.

Primero, el método de grabación: haz clic en el ícono de estrella y elige UART.

Menú de VS Code para elegir UART como método de grabación del ESP32

Después, el puerto: con la placa conectada, haz clic en el ícono del enchufe y selecciona el puerto que le corresponde.

Selección del puerto COM del ESP32 en la barra inferior de VS Code

Luego el chip: el ícono de chip en la misma barra. Elige el de tu placa.

Selección del chip ESP32-S3 como dispositivo de destino en VS Code

Con una ESP32-S3 programada por su USB nativo, falta elegir la configuración "ESP32-S chip (via builtin USB JTAG)".

Configuración del ESP32-S3 con USB JTAG integrado como destino en VS Code

Ahora compila con el ícono de llave inglesa (Build Project).

Botón de compilar proyecto en la barra inferior de VS Code con ESP IDF

La primera compilación tarda bastante más que las siguientes, porque arma todo el framework desde cero. Cuando termina, la terminal muestra el resumen y el mensaje "Build Successfully".

Terminal de VS Code con el mensaje de compilación exitosa del proyecto ESP IDF

Por último, graba la placa con el ícono del rayo ("Flash Device"). Según el modelo, puede que tengas que mantener apretado el botón BOOT para que entre en modo de grabación. Al terminar aparece el aviso "Flash Done".

Aviso de grabación terminada del proyecto ESP IDF en el ESP32

Probar la lectura

Abre el monitor con el ícono de pantalla ("Monitor Device") y presiona RESET en la placa. Vas a ver algo así:

Text
MAC: Base MAC Address (Factory): 68:B6:B3:22:9E:60
MAC: Wi-Fi STA MAC Address: 68:B6:B3:22:9E:60
MAC: Wi-Fi SoftAP MAC Address: 68:B6:B3:22:9E:61
MAC: Bluetooth MAC Address: 68:B6:B3:22:9E:62

Monitor serie con las MAC base, WiFi estación, SoftAP y Bluetooth del ESP32

Fíjate en el patrón: la base y la de estación son idénticas, la de SoftAP termina en 61 y la de Bluetooth en 62. Es justo la regla de "base + 1, base + 2" de más arriba, y es la prueba de que basta con mover la base para mover todas las demás.

Paso 2: fijar una MAC personalizada (y clonar la de un nodo dañado)

El segundo programa reemplaza la MAC base al arrancar y después lee la de estación para comprobar que el cambio se aplicó.

C
/*  
  Rui Santos & Sara Santos - Random Nerd Tutorials
  https://RandomNerdTutorials.com/esp-idf-esp32-mac-address/
*/
#include <stdio.h>
#include "esp_mac.h"
#include "esp_log.h"
#include "nvs_flash.h"

static const char *TAG = "CUSTOM_MAC";

// REPLACE THE NEXT VARIABLE WITH YOUR CUSTOM MAC ADDRESS
uint8_t custom_mac[6] = {0x32, 0xA1, 0xB2, 0xC3, 0xD4, 0xE5}; 

void app_main(void)
{  
    // Set custom MAC Address
    esp_err_t err = esp_base_mac_addr_set(custom_mac);
    if (err == ESP_OK) {
        ESP_LOGI(TAG, "Custom MAC address set successfully!");
    } else {
        ESP_LOGE(TAG, "Failed to set custom MAC address: %s", esp_err_to_name(err));
    }

    uint8_t read_mac[6];
    // Read new custom Wi-Fi STA MAC Address
    err = esp_read_mac(read_mac, ESP_MAC_WIFI_STA);
    if (err == ESP_OK) {
        ESP_LOGI(TAG, "[NEW] Wi-Fi STA MAC Address: %02X:%02X:%02X:%02X:%02X:%02X",
                 read_mac[0], read_mac[1], read_mac[2],
                 read_mac[3], read_mac[4], read_mac[5]);
    }
}

La dirección nueva se define en una sola línea:

C
uint8_t custom_mac[6] = {0x32, 0xA1, 0xB2, 0xC3, 0xD4, 0xE5};

Para clonar un nodo, aquí va la MAC de estación de la placa dañada. Si su MAC era 24:6F:28:A1:B2:C3, la línea queda {0x24, 0x6F, 0x28, 0xA1, 0xB2, 0xC3}. Como la de estación es igual a la base, al fijarla como base la placa nueva hereda también la de SoftAP y la de Bluetooth del nodo original.

Después viene la llamada que hace el trabajo, con su manejo de error:

C
esp_err_t err = esp_base_mac_addr_set(custom_mac);
if (err == ESP_OK) {
    ESP_LOGI(TAG, "Custom MAC address set successfully!");
} else {
    ESP_LOGE(TAG, "Failed to set custom MAC address: %s", esp_err_to_name(err));
}

Si la dirección es inválida, esp_err_to_name() traduce el código de error a texto legible en el log, lo que ahorra buscar números en la documentación.

Qué direcciones sirven y cuáles no

ESP IDF rechaza una MAC cuyo primer byte tenga el bit 0 en 1. Ese bit marca las direcciones de multidifusión (multicast), que identifican a un grupo de equipos y no a uno solo. Por eso 1A:XX:XX:XX:XX:XX es válida (0x1A termina en 0 en binario) y 15:XX:XX:XX:XX:XX no (0x15 termina en 1). Un truco rápido: el primer byte tiene que ser par.

Hay un segundo bit que conviene conocer aunque ESP IDF no lo exija. El bit 1 del primer byte indica si la dirección es administrada localmente (inventada por ti) o universal (asignada por un fabricante). El ejemplo del original usa 0x32, que en binario es 00110010: bit 0 en cero y bit 1 en uno, o sea una dirección local válida. Si inventas MAC para tus propios nodos, sigue esa convención (primer byte como 02, 06, 0A, 32...) y nunca vas a chocar con un equipo comercial de tu red. Para clonar, en cambio, usas la MAC de fábrica de la placa muerta, que es universal y también válida.

El momento justo: antes de iniciar el WiFi

esp_base_mac_addr_set() tiene que ejecutarse antes de inicializar cualquier interfaz de red, es decir, antes de esp_netif_init() y esp_wifi_init(). El driver de WiFi copia la MAC al iniciarse; si la cambias después, el radio sigue transmitiendo con la vieja y el log te dice que todo salió bien, lo que confunde bastante. En un nodo ESP NOW real, entonces, el orden de app_main() queda así:

  1. Fijar la MAC base con esp_base_mac_addr_set().
  2. Inicializar NVS (nvs_flash_init()), que el WiFi necesita.
  3. Inicializar esp_netif y el loop de eventos.
  4. esp_wifi_init(), modo estación y esp_wifi_start().
  5. esp_now_init() y el registro de peers.

Ese orden también explica la cabecera nvs_flash.h que el programa incluye sin usarla todavía: es la que vas a necesitar en cuanto sumes el WiFi.

Probar el cambio

Graba el programa con el mismo procedimiento del paso 1, abre el monitor y reinicia la placa. Deberías ver:

Text
CUSTOM_MAC: Custom MAC address set successfully!
CUSTOM_MAC: [NEW] Wi-Fi STA MAC Address: 32:A1:B2:C3:D4:E5

Monitor serie confirmando la MAC personalizada fijada en el ESP32

Clonar un nodo ESP NOW de principio a fin

Con las dos piezas anteriores, el reemplazo de una placa queda así:

  1. Recupera la MAC del nodo dañado. Si enciende, grábale el programa del paso 1 y anota la línea "WiFi STA". Si no, búscala en el código de los otros nodos, en tu registro o en la etiqueta de la caja.
  2. Retira la placa vieja de la red. Dos equipos con la misma MAC encendidos al mismo tiempo se pisan: los acuses de recibo de ESP NOW llegan a cualquiera de los dos y las entregas fallan de forma intermitente. Si la placa dañada todavía funciona a medias, desconéctala antes de seguir.
  3. Agrega la MAC al firmware del nodo. En el código que ya usabas, pon la llamada a esp_base_mac_addr_set() como primera línea de app_main(), antes de todo el arranque del WiFi.
  4. Graba la placa nueva y verifica en el monitor que la MAC de estación sea la de la placa original.
  5. Comprueba desde el receptor. Sin tocar su código, debería empezar a recibir los mensajes del nodo reemplazado como si nada hubiera pasado. Si usas el callback de envío, ESP_NOW_SEND_SUCCESS en el emisor confirma que el receptor lo reconoce.

Si la red usa cifrado con LMK, no tienes que hacer nada más: la clave está asociada a la MAC y la MAC es la misma.

Errores comunes y cómo resolverlos

  • El log dice que la MAC se fijó, pero los otros nodos no la reconocen. Casi siempre es el orden: la llamada quedó después de esp_wifi_init(). Muévela al principio de app_main().
  • ESP_ERR_INVALID_ARG al fijar la MAC. El primer byte es impar (bit de multidifusión en 1). Revísalo en binario o usa uno par.
  • Comunicación intermitente después del reemplazo. Probablemente la placa vieja sigue encendida en algún lado con la misma MAC.
  • El receptor recibe, pero el emisor no. Revisa que el receptor esté en el mismo canal WiFi que el emisor; la MAC no tiene nada que ver con el canal, pero es un problema que aparece seguido justo después de cambiar una placa.
  • No aparece el puerto al conectar la placa. Cambia el cable por uno de datos; en placas con USB nativo, prueba entrando a modo de grabación con BOOT + RESET.

Variantes y mejoras

  • Un solo firmware para todos los nodos. En vez de escribir la MAC en el código, guárdala en NVS (para eso está nvs_flash.h) y léela al arrancar. Así compilas una sola vez y cada placa conserva su identidad, aunque la reemplaces: basta con escribir la MAC del nodo viejo en la NVS de la placa nueva desde un comando por el puerto serie.
  • Cambiar solo la MAC de una interfaz. Si quieres que el WiFi tenga la MAC del nodo viejo pero el Bluetooth conserve la suya, usa esp_wifi_set_mac(WIFI_IF_STA, mac) después de esp_wifi_init() y antes de esp_wifi_start(), o esp_iface_mac_addr_set() en las versiones recientes de ESP IDF. Aplican las mismas reglas de bits.
  • Grabarla para siempre en eFuse, con mucho cuidado. El ESP32 tiene un bloque de eFuse para una MAC personalizada que se graba con espefuse.py burn_custom_mac y se lee con esp_efuse_mac_get_custom(). Es irreversible: si te equivocas en un byte, esa placa queda así para siempre. Úsalo solo en producción en serie, nunca para pruebas.

Personalización para Chile

En Chile consigues lo necesario para replicar y mantener esta red en MechatronicStore:

  • Placa de desarrollo ESP32-S3: es la que usa el tutorial original, con USB nativo y grabación por USB JTAG integrado. Sirve tanto para el nodo de pruebas como para la placa de reemplazo.
  • Placa ESP32 DevKit (ESP32-WROOM-32): si tu red ESP NOW ya está armada con ESP32 clásicos, mejor reemplazar con el mismo modelo. El código de esta guía funciona sin cambios en las dos familias.
  • Cable USB-C de datos: para grabar y monitorear la placa. Si tu DevKit es de los de micro USB, usa el cable que corresponda.

Un consejo para quienes mantienen redes de sensores: ten al menos una placa de repuesto del mismo modelo en el cajón. Con la MAC anotada y este procedimiento, cambiar un nodo dañado toma diez minutos en lugar de una tarde reprogramando la red entera.

Recursos

  • Tutorial original (inglés): ESP IDF: Read and Set ESP32 MAC Address, de Rui Santos y Sara Santos en Random Nerd Tutorials. Esta versión está basada en él, con el caso de uso de ESP NOW agregado.
  • Código: los dos programas completos (lectura y MAC personalizada) están en esta página, listos para copiar en main.c.
  • Documentación de Espressif: Misc System APIs (MAC Address) y ESP NOW.

Versión chilena con componentes en stock local en MechatronicStore.