ESP32 y Arduino

Configuración de ESP32 WiFiManager MQTT: ajusta el Wi-Fi y el broker en un portal cautivo

Deja de escribir credenciales en el código: permite que el usuario introduzca el Wi-Fi y el host, puerto, client ID, usuario y contraseña MQTT en un portal cautivo de WiFiManager, guárdalos con Preferences y conéctate a broker.connectmq.com por defecto.

Móvil mostrando un portal cautivo con campos del broker para una configuración ESP32 WiFiManager MQTT
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Con parámetros ESP32 WiFiManager MQTT, un dispositivo que entregas a otra persona se puede configurar desde un móvil: abre un portal cautivo, el usuario elige una red Wi-Fi y rellena los ajustes del broker, y el dispositivo lo recuerda todo entre reinicios. Este tutorial construye ese flujo con la librería WiFiManager y Preferences, usando broker.connectmq.com por defecto para que funcione nada más sacarlo de la caja.

El problema de los ajustes escritos en el código

La mayoría de los ejemplos MQTT empiezan con líneas como const char* WIFI_SSID = "..." y const char* MQTT_PASS = "...". En tu mesa no hay problema, pero cada red nueva o cambio de credenciales implica recompilar y volver a flashear. También significa que el binario del firmware contiene secretos y que no puedes dar el mismo binario a dos clientes.

Un portal de aprovisionamiento resuelve esto. En el primer arranque, o cuando el usuario mantiene pulsado un botón, el ESP32 inicia su propio punto de acceso. Al conectarse a él con un móvil se abre una página web donde el usuario introduce:

  • La red Wi-Fi y su contraseña.
  • El host y el puerto del broker MQTT.
  • El client ID, el usuario y la contraseña MQTT.
  • Un prefijo de topic para este dispositivo.

El dispositivo guarda los valores en almacenamiento no volátil, se une a la red Wi-Fi y se conecta al broker. El mismo binario sirve para todo el mundo.

Librerías y hardware

  • WiFiManager de tzapu (2.x), instalable desde el gestor de librerías de Arduino o desde PlatformIO como tzapu/WiFiManager.
  • PubSubClient para MQTT.
  • Preferences, que viene con el core Arduino-ESP32 y guarda pares clave–valor en la flash NVS.
  • Cualquier placa ESP32. El ejemplo usa el botón BOOT (GPIO 0) de la mayoría de los DevKit para volver a abrir el portal.

Cómo funciona el flujo ESP32 WiFiManager MQTT

Situación Qué ocurre
Primer arranque, sin Wi-Fi guardado El portal arranca como punto de acceso Sensor-Setup-xxxx
Wi-Fi guardado accesible El dispositivo se conecta sin más y carga los ajustes MQTT desde Preferences
Wi-Fi guardado inaccesible El portal vuelve a abrirse tras fallar el intento de conexión
Botón BOOT pulsado 3 s al arrancar El portal se abre a demanda para que el usuario cambie los ajustes
Portal sin usar durante 3 minutos El dispositivo se reinicia y vuelve a intentarlo, así que nunca se queda colgado para siempre

WiFiManager guarda por sí mismo las credenciales Wi-Fi. Los campos MQTT son parámetros personalizados que añades al portal y que persistes tú.

Sketch completo

#include <WiFi.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <PubSubClient.h>

const int CONFIG_BUTTON = 0;    // botón BOOT en la mayoría de los ESP32 DevKit

Preferences prefs;
WiFiClient net;
PubSubClient mqtt(net);

char mqttHost[64]   = "broker.connectmq.com";
char mqttPort[6]    = "1883";
char mqttClient[48] = "";
char mqttUser[48]   = "";
char mqttPass[64]   = "";
char topicPrefix[64] = "connectmq/demo/d93f2a";

bool shouldSave = false;
unsigned long lastPublish = 0;

void loadSettings() {
  prefs.begin("mqtt", true);
  prefs.getString("host", mqttHost, sizeof(mqttHost));
  prefs.getString("port", mqttPort, sizeof(mqttPort));
  prefs.getString("client", mqttClient, sizeof(mqttClient));
  prefs.getString("user", mqttUser, sizeof(mqttUser));
  prefs.getString("pass", mqttPass, sizeof(mqttPass));
  prefs.getString("prefix", topicPrefix, sizeof(topicPrefix));
  prefs.end();

  if (strlen(mqttClient) == 0) {   // no conectar nunca con un client ID vacío
    snprintf(mqttClient, sizeof(mqttClient), "esp32-%08x", (uint32_t)ESP.getEfuseMac());
  }
}

void saveSettings() {
  prefs.begin("mqtt", false);
  prefs.putString("host", mqttHost);
  prefs.putString("port", mqttPort);
  prefs.putString("client", mqttClient);
  prefs.putString("user", mqttUser);
  prefs.putString("pass", mqttPass);
  prefs.putString("prefix", topicPrefix);
  prefs.end();
}

void runWifiManager(bool forcePortal) {
  WiFiManager wm;

  WiFiManagerParameter pHost("host", "MQTT host", mqttHost, sizeof(mqttHost) - 1);
  WiFiManagerParameter pPort("port", "MQTT port (1883 or 8883)", mqttPort, sizeof(mqttPort) - 1);
  WiFiManagerParameter pClient("client", "MQTT client ID", mqttClient, sizeof(mqttClient) - 1);
  WiFiManagerParameter pUser("user", "MQTT username (optional)", mqttUser, sizeof(mqttUser) - 1);
  WiFiManagerParameter pPass("pass", "MQTT password (optional)", mqttPass, sizeof(mqttPass) - 1,
                             "type='password'");
  WiFiManagerParameter pPrefix("prefix", "Topic prefix", topicPrefix, sizeof(topicPrefix) - 1);

  wm.addParameter(&pHost);
  wm.addParameter(&pPort);
  wm.addParameter(&pClient);
  wm.addParameter(&pUser);
  wm.addParameter(&pPass);
  wm.addParameter(&pPrefix);

  wm.setSaveParamsCallback([]() { shouldSave = true; });
  wm.setConfigPortalTimeout(180);

  char apName[32];
  snprintf(apName, sizeof(apName), "Sensor-Setup-%04x", (uint16_t)ESP.getEfuseMac());

  bool connected = forcePortal ? wm.startConfigPortal(apName)
                               : wm.autoConnect(apName);
  if (!connected) {
    ESP.restart();               // el portal agotó el tiempo: volver a empezar desde cero
  }

  if (shouldSave) {
    strlcpy(mqttHost, pHost.getValue(), sizeof(mqttHost));
    strlcpy(mqttPort, pPort.getValue(), sizeof(mqttPort));
    strlcpy(mqttClient, pClient.getValue(), sizeof(mqttClient));
    strlcpy(mqttUser, pUser.getValue(), sizeof(mqttUser));
    strlcpy(mqttPass, pPass.getValue(), sizeof(mqttPass));
    strlcpy(topicPrefix, pPrefix.getValue(), sizeof(topicPrefix));
    saveSettings();
    loadSettings();              // vuelve a aplicar el client ID de respaldo
  }
}

bool buttonHeld(unsigned long ms) {
  pinMode(CONFIG_BUTTON, INPUT_PULLUP);
  unsigned long start = millis();
  while (digitalRead(CONFIG_BUTTON) == LOW) {
    if (millis() - start > ms) return true;
    delay(10);
  }
  return false;
}

void connectMqtt() {
  while (!mqtt.connected()) {
    bool ok = strlen(mqttUser) > 0
                ? mqtt.connect(mqttClient, mqttUser, mqttPass)
                : mqtt.connect(mqttClient);
    if (ok) {
      String cmd = String(topicPrefix) + "/cmd";
      mqtt.subscribe(cmd.c_str());
    } else {
      Serial.printf("MQTT failed state=%d, retrying\n", mqtt.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  loadSettings();
  delay(200);
  runWifiManager(buttonHeld(3000));

  mqtt.setServer(mqttHost, atoi(mqttPort));
  Serial.printf("Broker %s:%s as %s\n", mqttHost, mqttPort, mqttClient);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  if (millis() - lastPublish > 10000) {
    lastPublish = millis();
    String topic = String(topicPrefix) + "/telemetry";
    String payload = "{\"uptime\":" + String(millis() / 1000) + "}";
    mqtt.publish(topic.c_str(), payload.c_str());
  }
}

Recorrido por el portal

  1. Flashea el sketch y abre el monitor serie.
  2. En tu móvil, únete a la red Wi-Fi llamada Sensor-Setup-xxxx. La mayoría de los móviles abren el portal cautivo automáticamente; si no, navega a 192.168.4.1.
  3. Pulsa Configure WiFi, elige tu red e introduce su contraseña.
  4. Deja los valores por defecto para usar el área pública de broker.connectmq.com, o introduce tus propios ajustes, y guarda.
  5. El dispositivo se une a tu red, se conecta al broker y empieza a publicar en <prefix>/telemetry.

Compruébalo desde un ordenador:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-d93f2a -t 'connectmq/demo/d93f2a/#' -v

Todos los dispositivos que mantengan el prefijo por defecto publican en el mismo topic, así que si configuras varios, da a cada uno su propio prefijo en el portal.

Detalles que lo hacen fiable

Carga antes de construir el portal. Los parámetros personalizados se crean con los valores actuales como valores por defecto, así que el portal muestra lo que está guardado y no un formulario en blanco. Un usuario que edita un campo no pierde los demás.

Nunca te conectes con un client ID vacío. Si el usuario borra el campo, loadSettings() recurre a un ID derivado de la MAC del chip. Un client ID vacío o duplicado es uno de los principales motivos por los que los dispositivos no consiguen conectar o se expulsan mutuamente; consulta por qué un ESP32 no puede conectarse a un broker MQTT para ver la lista completa.

Usa el timeout del portal. Sin setConfigPortalTimeout(), un dispositivo cuyo router está temporalmente caído se queda en modo portal para siempre. Con un timeout, se reinicia y vuelve a intentar la red guardada.

Guarda los números como cadenas en el portal y conviértelos una vez. Los parámetros de WiFiManager son campos de texto. Mantén el puerto como cadena en el almacenamiento y conviértelo con atoi() al configurar el cliente.

Oculta el campo de la contraseña. El argumento extra type='password' enmascara la contraseña MQTT en el portal. El valor se sigue enviando por el punto de acceso abierto en HTTP sin cifrar, así que aprovisiona solo en un lugar de confianza.

Validar lo que escribe el usuario

Un portal cautivo acepta cualquier cosa que introduzca el usuario, así que el firmware tiene que lidiar con los errores. Algunas comprobaciones tras guardar evitan que un dispositivo quede inservible por unos ajustes erróneos:

  • Puerto: acepta solo valores entre 1 y 65535. Si atoi() devuelve 0 porque el campo contenía texto, recurre al 1883.
  • Host: elimina los espacios y rechaza una cadena vacía. Los usuarios suelen pegar una URL como mqtt://broker.connectmq.com; quita el esquema y todo lo que vaya tras el nombre de host.
  • Prefijo de topic: elimina la barra final y rechaza los comodines (+ y #), que no se permiten en los topics de publicación.
  • Credenciales: si hay usuario pero la contraseña está vacía, avisa por la consola serie, porque el broker rechazará la conexión.

Cuando la conexión MQTT falla repetidamente con un código mqtt.state() positivo (credenciales incorrectas o no autorizado), suele ser un problema de ajustes y no de red. Un buen patrón es contar esos fallos y volver a abrir el portal automáticamente tras varios seguidos, para que el usuario pueda corregir los valores sin necesidad de un botón ni de un cable serie.

Añadir TLS para brokers de producción

El puerto 1883 va en texto plano y vale para el área pública de pruebas. Para un broker privado, cambia a WiFiClientSecure cuando el usuario elija el puerto 8883:

#include <WiFiClientSecure.h>

WiFiClientSecure secureNet;

void configureTransport() {
  if (atoi(mqttPort) == 8883) {
    secureNet.setCACert(ISRG_ROOT_X1);   // raíz de Let's Encrypt, guardada como cadena PEM
    mqtt.setClient(secureNet);
  } else {
    mqtt.setClient(net);
  }
  mqtt.setServer(mqttHost, atoi(mqttPort));
}

Llama a configureTransport() en lugar de la línea setServer() simple en setup(). Con ConnectMQ, los usuarios introducen entonces en el portal el client ID, el usuario y la contraseña generados para su broker (YOUR_CLIENT_ID, YOUR_USERNAME, YOUR_PASSWORD). El client ID está vinculado a esas credenciales, así que debe introducirse exactamente como se emitió. La guía de ESP32 MQTT SSL/TLS explica cómo incrustar el certificado raíz.

Solución de problemas

Problema Causa probable Solución
El portal nunca aparece El Wi-Fi guardado sigue conectando Mantén pulsado el botón BOOT durante el reset, o llama una vez a wm.resetSettings()
Los parámetros no se guardan No se dispara el callback de guardado Usa setSaveParamsCallback() y pulsa Save en la página del portal
Valores truncados Longitud del campo menor que la entrada Aumenta el tamaño del búfer y la longitud del parámetro
Conecta al Wi-Fi pero no a MQTT Puerto incorrecto para el transporte o credenciales incorrectas Revisa mqtt.state() y la combinación puerto/TLS
El móvil no abre el portal Detección del portal cautivo bloqueada Navega manualmente a 192.168.4.1

Próximos pasos

Una vez que los ajustes son configurables, los siguientes pasos naturales son las actualizaciones remotas y los informes de estado: actualizaciones OTA del ESP32 lanzadas por MQTT muestra cómo distribuir nuevo firmware sin USB, y el tutorial de IoT con ESP32 integra el aprovisionamiento en un proyecto de dispositivo completo.

Aprovisiona dispositivos para ConnectMQ

Los ajustes por defecto apuntan al servidor MQTT público gratuito, así que un dispositivo nuevo funciona en cuanto se une al Wi-Fi; úsalo solo para pruebas y prototipos, no para datos de producción. Para los despliegues, crea un broker privado en ConnectMQ e introduce sus credenciales en el portal. ConnectMQ es un broker MQTT gestionado, así que quienes configuran los dispositivos nunca necesitan ejecutar un servidor. Empieza en el plan gratuito Serverless en app.connectmq.com, sin tarjeta de crédito.

Preguntas frecuentes

¿Dónde guarda WiFiManager la contraseña del Wi-Fi?

El driver Wi-Fi del ESP32 guarda las credenciales de red en la flash NVS. En este sketch, los campos MQTT se guardan por separado con Preferences bajo el espacio de nombres mqtt.

¿Cómo vuelvo a abrir más tarde el portal de WiFiManager?

En este sketch, mantén pulsado el botón BOOT durante tres segundos al arrancar, lo que llama a startConfigPortal(). También podrías lanzarlo con un comando MQTT, siempre que el dispositivo siga siendo accesible.

¿Puedo usar WiFiManager con AsyncMqttClient?

Sí. WiFiManager solo se encarga del aprovisionamiento. Cuando termina, usa los valores guardados para configurar el cliente MQTT que prefieras.

¿Es seguro el portal cautivo?

El portal funciona como un punto de acceso abierto sobre HTTP sin cifrar, así que cualquiera cerca podría observar los valores durante la configuración. Aprovisiona en un lugar de confianza y plantéate poner una contraseña al punto de acceso de configuración.

Artículos relacionados

Ejemplo de ESP-IDF MQTT: crea un cliente esp-mqtt desde cero

Construye un proyecto ESP-IDF desde una carpeta vacía con el componente esp-mqtt: configuración de Wi-Fi en modo estación, el manejador de eventos MQTT, publicación y suscripción, propiedades de MQTT 5 y TLS con el bundle de certificados.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.