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
- El ESP32 arranca y no encuentra configuración en NVS (parte 6).
- Crea una red abierta llamada, por ejemplo,
Casa-Setup-A1B2. - Te conectas con el celular y la página de configuración se abre sola.
- Escribes la red Wi-Fi y su contraseña.
- El ESP32 las guarda en NVS y se reinicia.
- 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:
- Los últimos caracteres de la MAC en el nombre de la red distinguen dos dispositivos nuevos encendidos a la vez.
WiFi.softAPmacAddress()en lugar deWiFi.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
onNotFoundson 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);
}
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.

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.