DEV Community

Cover image for Portal cautivo en ESP32: configurar el Wi-Fi sin reprogramar
Steven Carvajal
Steven Carvajal

Posted on Originally published at iot.gripe

Portal cautivo en ESP32: configurar el Wi-Fi sin reprogramar

Publicado originalmente en iot.gripe, como parte 16 de la serie "Domótica con ESP32 y AWS desde cero".

Escribir el nombre y la contraseña del Wi-Fi en el código funciona en tu escritorio, pero no si el dispositivo va a otra casa o si cambias de router. La solución que usan casi todos los productos comerciales: cuando el dispositivo no tiene configuración, levanta su propia red Wi-Fi con una página web donde escribes los datos desde el celular.

En esta parte construimos ese portal en un ESP32 con Arduino: punto de acceso, formulario, servidor DNS para que la página se abra sola, guardado en NVS y vuelta al portal si la red falla.

El flujo

  1. El ESP32 arranca y no encuentra configuración en NVS (parte 6).
  2. Crea una red abierta llamada, por ejemplo, Casa-Setup-A1B2.
  3. Te conectas con el celular y la página de configuración se abre sola.
  4. Escribes la red Wi-Fi y su contraseña.
  5. El ESP32 las guarda en NVS y se reinicia.
  6. Arranca en modo normal y se conecta a tu red. Si no puede, vuelve al portal.

El firmware: dos modos en un solo binario

#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>

WebServer servidor(80);
DNSServer dns;
Preferences prefs;
bool modoPortal = false;

void setup() {
  Serial.begin(115200);

  prefs.begin("wifi", true);
  String ssid = prefs.getString("ssid", "");
  String pass = prefs.getString("pass", "");
  prefs.end();

  if (ssid.isEmpty() || !conectarWiFi(ssid, pass)) {
    iniciarPortal();
    return;
  }
  Serial.printf("Conectado a %s con IP %s\n", ssid.c_str(), WiFi.localIP().toString().c_str());
  // modo normal: nube, sensores, etc.
}

void loop() {
  if (modoPortal) {
    dns.processNextRequest();
    servidor.handleClient();
    return;
  }
  // modo normal
}
Enter fullscreen mode Exit fullscreen mode

Intentar conectar con un límite de tiempo

bool conectarWiFi(const String& ssid, const String& pass) {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), pass.c_str());
  Serial.printf("Conectando a %s", ssid.c_str());
  unsigned long inicio = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - inicio < 15000) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  return WiFi.status() == WL_CONNECTED;
}
Enter fullscreen mode Exit fullscreen mode

Sin el límite de 15 segundos, una contraseña mal escrita dejaría el dispositivo esperando para siempre, sin forma de corregirla.

Levantar la red y el portal

void iniciarPortal() {
  modoPortal = true;
  WiFi.mode(WIFI_AP);
  delay(100);  // en algunos chips la MAC del AP no está lista al instante

  String mac = WiFi.softAPmacAddress();  // en modo AP, macAddress() puede devolver ceros
  mac.replace(":", "");
  String nombre = "Casa-Setup-" + mac.substring(mac.length() - 4);
  WiFi.softAP(nombre.c_str());

  // DNS: cualquier dominio apunta al propio ESP32
  dns.start(53, "*", WiFi.softAPIP());

  servidor.on("/", HTTP_GET, paginaFormulario);
  servidor.on("/guardar", HTTP_POST, guardar);
  servidor.onNotFound(paginaFormulario);  // cualquier URL muestra el formulario
  servidor.begin();

  Serial.printf("Portal activo: conéctate a '%s'\n", nombre.c_str());
}
Enter fullscreen mode Exit fullscreen mode

Tres detalles:

  • Los últimos caracteres de la MAC en el nombre de la red distinguen dos dispositivos nuevos encendidos a la vez.
  • WiFi.softAPmacAddress() en lugar de WiFi.macAddress(): en modo punto de acceso, la MAC de modo estación puede no estar disponible y todas las placas acabarían con el mismo nombre de red.
  • El servidor DNS y onNotFound son lo que convierte esto en un portal cautivo: cuando el celular se conecta, comprueba si hay internet pidiendo una URL conocida; el ESP32 responde con su formulario y el sistema abre la página sola.

El formulario

void paginaFormulario() {
  String html =
      "<!doctype html><html><head><meta name='viewport' content='width=device-width'></head>"
      "<body style='font-family:sans-serif;max-width:420px;margin:40px auto;padding:0 16px'>"
      "<h2>Configurar Wi-Fi</h2>"
      "<form method='POST' action='/guardar'>"
      "<label>Red</label><br><input name='ssid' required style='width:100%'><br><br>"
      "<label>Contraseña</label><br><input name='pass' type='password' style='width:100%'><br><br>"
      "<button type='submit'>Guardar</button>"
      "</form></body></html>";
  servidor.send(200, "text/html", html);
}
Enter fullscreen mode Exit fullscreen mode

Una mejora habitual: escanear las redes cercanas con WiFi.scanNetworks() y mostrarlas en una lista, para que el usuario no tenga que escribir el nombre.

Validar, guardar y reiniciar

void guardar() {
  String ssid = servidor.arg("ssid");
  String pass = servidor.arg("pass");
  ssid.trim();

  if (ssid.isEmpty() || ssid.length() > 32 || pass.length() > 63) {
    servidor.send(400, "text/plain", "Datos inválidos");
    return;
  }

  prefs.begin("wifi", false);
  prefs.putString("ssid", ssid);
  prefs.putString("pass", pass);
  prefs.end();

  servidor.send(200, "text/html", "<p>Guardado. El dispositivo se reinicia y se conecta a tu red.</p>");
  delay(500);  // deja que la respuesta llegue al celular
  ESP.restart();
}
Enter fullscreen mode Exit fullscreen mode

El delay(500) antes de reiniciar es necesario: sin él, el ESP32 se reinicia antes de terminar de enviar la página y el celular muestra un error aunque todo salió bien.

Monitor Serie de un ESP32 con portal cautivo: arranca sin configuración, levanta su red, recibe el formulario, se reinicia y se conecta a la red de la casa

Salida esperada en el Monitor Serie durante una configuración completa. Los nombres e IP son de ejemplo.

Seguridad

  • La red de configuración está abierta, pero solo existe mientras el dispositivo no está configurado. Si te preocupa que alguien cercano lo configure antes que tú, ponle una contraseña impresa en una etiqueta del dispositivo.
  • Cierra el portal después de un tiempo (por ejemplo, 10 minutos) y reinicia, para no dejar una red abierta indefinidamente si nadie lo configura.
  • Ofrece una forma de volver al portal: mantener pulsado un botón al arrancar que borre la configuración de NVS.
  • La contraseña viaja sin cifrar dentro de la red del ESP32 (HTTP). Para un producto, considera el aprovisionamiento oficial de Espressif, que cifra el intercambio.

Alternativas

Opción Ventajas Inconvenientes
Portal propio (este artículo) Control total de campos y diseño Más código que mantener
WiFiManager (librería) Portal cautivo completo en pocas líneas Menos flexible si necesitas campos especiales
Aprovisionamiento de Espressif (SoftAP o BLE, con su app) Intercambio cifrado, app oficial Depende de la app de Espressif o de crear la tuya
Improv Wi-Fi Configura por USB o BLE desde el navegador Requiere un navegador compatible

Si además del Wi-Fi el dispositivo necesita una identidad en la nube (un certificado), eso se resuelve aparte: la parte 17 explica Fleet Provisioning.

Preguntas frecuentes

¿Por qué el celular no abre la página solo?

Falta el servidor DNS o el onNotFound. Algunos sistemas también tardan unos segundos en detectar el portal; mientras tanto, abre http://192.168.4.1 a mano.

¿El ESP32 puede mantener el portal y conectarse a la vez?

Sí, con WIFI_AP_STA, útil para probar la conexión antes de guardar. Ten en cuenta que en ese modo el punto de acceso pasa al canal de la red a la que se conecta, y el celular puede desconectarse un momento.

¿Funciona con redes de 5 GHz?

No. El ESP32 clásico, el S3 y el C3 solo tienen Wi-Fi de 2,4 GHz. Si tu router combina ambas bandas con el mismo nombre, normalmente funciona; si son redes separadas, elige la de 2,4 GHz.

Top comments (4)

Collapse
 
_firelinks profile image
Mike Dabydeen •

Yo cambiaría la condición para entrar al portal, porque hoy un corte de luz deja el dispositivo en modo configuración con la red abierta. Con datos ya guardados, setup() llama a iniciarPortal() si conectarWiFi no conecta en 15 segundos, y el ESP32 arranca en un par de segundos, mientras que un router casero suele tardar más que eso en volver. Una vez en el portal, loop() solo atiende DNS y HTTP y nunca vuelve a intentar la red guardada, así que se queda ahí hasta que alguien lo configure de nuevo. Y ese alguien puede ser cualquier persona cerca, que lo apunta a su propia red.

Abriría el portal automáticamente solo cuando NVS está vacío. Con credenciales guardadas, reintentaría con una espera creciente y dejaría el portal para el botón al arrancar que mencionas en la sección de seguridad. Si prefieres mantener el portal como respaldo, WIFI_AP_STA te permite seguir intentando la red guardada mientras está abierto y cerrarlo en cuanto conecte.

Collapse
 
stevencarvajal profile image
Steven Carvajal • • Edited

Gracias por tomarte el tiempo de leer el código con tanto detalle. Tu observación es muy acertada: el escenario del corte de luz es justo el tipo de caso real que se me pasó. El ESP32 arranca más rápido que el router, y quedarse en modo configuración con la red abierta es un riesgo innecesario.

Me gusta tu propuesta de abrir el portal solo cuando la NVS esté vacía, reintentar con espera creciente cuando ya hay credenciales y dejar el portal para el botón al arrancar. Voy a evaluarla junto con la alternativa de WIFI_AP_STA, que permite mantener el portal como respaldo sin dejar de intentar la red guardada. Como no tengo OTA, tengo que cuidar que el cambio no obligue a reflashear placas ya instaladas.

Actualmente el portal solo se abre cuando la placa aún no ha sido reclamada (NVS vacía). Con credenciales ya guardadas, el maestro nunca levanta el AP: si el router tarda en volver tras un corte de luz, reintenta la conexión hasta lograrla. Aun así, tu idea de una espera creciente en los reintentos me parece buena y la tendré en cuenta. Gracias por el aporte.

Si lo implemento, lo dejo documentado en el blog y te menciono. Gracias de nuevo por el aporte, comentarios así mejoran el proyecto.

Collapse
 
_firelinks profile image
Mike Dabydeen •

El límite que yo dejaría explícito es que el portal de configuración no debe convertirse en el mecanismo normal de recuperación. Si las credenciales están guardadas, mantendría el AP cerrado durante los reintentos y abriría el portal solo mediante una acción física con una ventana temporal, idealmente con un indicador claro de que está en modo recuperación. Así un reinicio por corte no expone la red, pero un cambio de contraseña del router no deja la placa sin camino de vuelta. Con espera creciente y un máximo, además evitas que el equipo consuma energía en reintentos indefinidos. Si no hay OTA, documentaría también qué ocurre si se pulsa el botón durante el arranque. ¿Qué señal física usarías para confirmar que el portal quedó abierto?

Thread Thread
 
stevencarvajal profile image
Steven Carvajal •

Tienes toda la razón, amigo. Muchísimas gracias, me hiciste caer en cuenta de que no tengo un modo de recuperación si cambian la contraseña del Wi-Fi.

Pero, en tal caso, usaría un LED de estado con un patrón distinto para el modo de recuperación; por ejemplo, un parpadeo lento mientras el AP está abierto, que se apague al cerrarse la ventana. Es la única señal que no requiere un teléfono, e initStatusLed() ya controla el LED.