Cloud, DevOps e IoT en español

Parte 16 de 22 de la serie Domótica con ESP32 y AWS desde cero

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

9 de octubre de 2026 · Steven Carvajal · Tutoriales

Código de este artículo en GitHub →

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
}

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;
}

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());
}

Tres detalles:

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);
}

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();
}

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

Alternativas

OpciónVentajasInconvenientes
Portal propio (este artículo)Control total de campos y diseñoMás código que mantener
WiFiManager (librería)Portal cautivo completo en pocas líneasMenos flexible si necesitas campos especiales
Aprovisionamiento de Espressif (SoftAP o BLE, con su app)Intercambio cifrado, app oficialDepende de la app de Espressif o de crear la tuya
Improv Wi-FiConfigura por USB o BLE desde el navegadorRequiere 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.

Sigue leyendo