
Cómo crear un portal Wi-Fi embebido en el ESP32
Portal del técnico para configurar Wi-Fi sin cable desde el móvil: escaneo de redes, almacenamiento en NVS y prueba de conexión sin bloquear HTTP.
Requisitos de hardware:
- Placa ESP32 ou ESP32-S3 (referência: módulo N16R8)
- Cabo USB com transmissão de dados
- Um celular para abrir o portal
- Rede Wi-Fi de 2,4 GHz
Descarga el material de este tutorial:
- Projeto completo (ZIP)153 KBFirmware Arduino com o portal, varredura de redes e gravação na NVS, definição da placa N16R8, modelo de configuração, README e o manual em PDF.
- Manual em PDF (7 páginas)168 KBPasso a passo em oito etapas, o padrão de resposta que evita travar o portal, tabela de solução de problemas e checklist final.
¿Quieres recibir nuevos manuales y esquemas industriales? Únete a nuestro canal de Telegram / WhatsApp
El problema que resuelve el portal
Después de grabar el firmware, el dispositivo todavía no sabe a qué red conectarse. Volver a compilar con el SSID y la contraseña incluidos funciona en el banco de pruebas, pero no en campo: cada instalación necesitaría un binario diferente y el técnico necesitaría un portátil con el entorno de compilación solo para cambiar una contraseña.
El portal integrado invierte esa situación. La placa crea su propia red, el técnico conecta el teléfono, abre una página, elige la red e introduce la contraseña. Las credenciales se guardan en memoria no volátil y sobreviven al reinicio y a una nueva grabación. Sin cable y sin compilador.
Este tutorial documenta el portal del firmware AnalictY y complementa el tutorial de NAT: uno configura el dispositivo y el otro enruta su tráfico.
Aquí sirve Arduino, pero no para NAT
Conviene explicar la elección, porque al principio puede parecer incoherente.
El repetidor NAT necesita ESP-IDF porque NAPT depende de opciones de compilación de lwIP que Arduino Core no expone. El portal de configuración no necesita nada de eso: requiere Wi-Fi en modo punto de acceso, un servidor HTTP y almacenamiento persistente. Los tres existen en Arduino Core, listos y estables.
Usar ESP-IDF aquí solo añadiría complejidad sin beneficio. La capa se elige según el requisito, no según la preferencia.
WebServer síncrono, no asíncrono
Muchos tutoriales eligen ESPAsyncWebServer por costumbre. Este portal usa el WebServer que ya incluye el core:
| 1 | #include <WiFi.h> |
| 2 | #include <WebServer.h> |
| 3 | #include <Preferences.h> |
| 4 | WebServer webServer(80); |
| 5 | Preferences preferences; |
El servidor síncrono atiende una petición por vez en el bucle principal. Para un portal que un técnico abre durante dos minutos, es más que suficiente y evita una dependencia externa que suele provocar conflictos de versión en ESP32.
La regla práctica es sencilla: lo asíncrono compensa cuando hay varios clientes simultáneos o conexiones largas, como WebSocket. El aprovisionamiento no es ese caso.
Dónde vive el HTML y por qué no es LittleFS
Otro reflejo común es guardar la página en un sistema de archivos. Este portal integra el HTML directamente en el binario y usa la macro F para mantener el texto en la flash en lugar de la RAM:
| 1 | page += F("<div class='api'>Telefono en el AP: http://192.168.4.1<br>"); |
| 2 | page += F("GET /api/status<br>GET /api/wifi/scan<br>"); |
| 3 | page += F("GET /api/config<br>POST /api/config</div>"); |
La ventaja operativa es importante: el firmware es un solo archivo. No existe una etapa separada para subir la imagen del sistema de archivos ni el riesgo clásico de grabar el firmware nuevo y olvidar actualizar la interfaz, dejando una página antigua sirviendo endpoints que ya cambiaron.
El costo es que cambiar la interfaz exige recompilar. Para una página de configuración que cambia poco, es un intercambio conveniente.
Las rutas del portal
Hay nueve rutas, cada una con su variante OPTIONS para responder a CORS cuando otra origen consume el portal:
| 1 | GET / pagina del portal |
| 2 | GET /api/status diagnostico en JSON |
| 3 | GET /api/capabilities capacidades de la placa |
| 4 | GET /api/wifi/scan redes encontradas en el escaneo |
| 5 | GET /api/config configuracion actual |
| 6 | POST /api/config guarda SSID, contraseña y parametros |
| 7 | GET /api/program programa del runtime |
| 8 | POST /api/program guarda el programa |
| 9 | GET /api/runtime/status estado de ejecucion |
| 10 | POST /api/reboot reinicia la placa |
| 11 | POST /api/reset-config borra la configuracion de la NVS |
Separar lectura y escritura en métodos distintos de la misma ruta mantiene la API predecible: GET en `/api/config` devuelve lo guardado y POST en la misma ruta lo guarda.
El escaneo que no abandona al primer intento
El escaneo Wi-Fi en ESP32 falla con frecuencia cuando la radio está ocupada, especialmente con el punto de acceso activo. Un escaneo que devuelve error hace que el técnico piense que la red no existe, aunque sí esté allí.
Por eso el escaneo intenta dos veces, detiene el anterior y borra el resultado antes de repetir:
| 1 | int scanWifiNetworks() { |
| 2 | for (int attempt = 0; attempt < 2; attempt++) { |
| 3 | prepareWifiScanMode(); |
| 4 | esp_wifi_scan_stop(); |
| 5 | WiFi.scanDelete(); |
| 6 | delay(attempt == 0 ? 120 : 450); |
| 7 | int count = WiFi.scanNetworks(false, true, false, 650); |
| 8 | if (count >= 0) { |
| 9 | return count; |
| 10 | } |
| 11 | WiFi.scanDelete(); |
| 12 | WiFi.mode(apMode ? WIFI_AP_STA : WIFI_STA); |
| 13 | delay(350); |
| 14 | } |
| 15 | return -1; |
| 16 | } |
Los cuatro argumentos importan. El primer false hace que el escaneo sea bloqueante y devuelva el resultado listo. El segundo true incluye redes ocultas. El tercer false usa un escaneo activo, que encuentra más redes que el pasivo. Y 650 es el tiempo en milisegundos por canal; por debajo de eso, las redes lejanas desaparecen del resultado.
El segundo intento espera más que el primero, 450 en lugar de 120 milisegundos, para que la radio pueda reorganizarse antes de repetir.
El detalle que separa un buen portal de uno malo
Este es el punto central del artículo.
La tentación es recibir el formulario, conectarse al Wi-Fi y solo después responder al navegador con el resultado. No lo hagas. Si la contraseña es incorrecta, el ESP32 puede pasar hasta veinte segundos intentándolo, el teléfono agota el tiempo de espera y la página muere sin respuesta. El técnico concluye que no se guardó, vuelve a introducir el dato y encuentra el mismo problema.
La secuencia correcta es guardar, responder inmediatamente y probar después:
| 1 | void handleConfigPost() { |
| 2 | saveConfigFromRequest(); |
| 3 | lastWifiStatus = WL_IDLE_STATUS; |
| 4 | lastWifiError = "Configuracion guardada. Prueba Wi-Fi iniciada..."; |
| 5 | wifiConnectTestPending = true; |
| 6 | String body; |
| 7 | body += F("<meta http-equiv='refresh' content='8;url=/'>"); |
| 8 | body += F("<h1>Configuracion guardada</h1>"); |
| 9 | webServer.send(200, "text/html", body); |
| 10 | } |
El guardado ocurre, la respuesta sale de inmediato y la bandera queda marcada. La prueba de conexión se ejecuta en el bucle principal, fuera del ciclo de la petición:
| 1 | if (wifiConnectTestPending) { |
| 2 | wifiConnectTestPending = false; |
| 3 | connectWiFiOnce(20000); |
| 4 | } |
La página que recibe el técnico incluye un meta refresh de 8 segundos hacia la raíz. No tiene que hacer nada: la pantalla vuelve sola al portal y ahora muestra si conectó o por qué falló.
La experiencia mejora muchísimo. Guardar siempre confirma al instante. La prueba, que es lenta y puede fallar, ocurre por separado.
Diagnóstico que indica qué hacer
Un mensaje de error genérico durante el aprovisionamiento no sirve. El portal expone el último estado en `/api/status`, con la causa probable ya traducida:
| 1 | { |
| 2 | "wifi_connected": false, |
| 3 | "wifi_ssid": "MiRed-2.4G", |
| 4 | "last_wifi_status": "desconectado", |
| 5 | "last_wifi_error": "Fallo: desconectado. Normalmente contraseña incorrecta, WPA3-only, filtro MAC o rechazo del router.", |
| 6 | "last_wifi_channel": 6, |
| 7 | "ap_active": true, |
| 8 | "ap_ip": "192.168.4.1" |
| 9 | } |
Observa que ap_active sigue siendo verdadero después del fallo. Es el contrato del portal: el punto de acceso no se apaga cuando falla la conexión. Si lo hiciera, el técnico perdería el camino de regreso y necesitaría un cable, exactamente lo que el portal existe para evitar.
Cuando conecta, el mismo endpoint devuelve la dirección asignada y la intensidad de la señal, lo que ayuda a decidir la posición de la antena en campo.
Tutorial: de cero a Wi-Fi configurado por el teléfono
Paso 1: preparar el entorno
Instala VS Code y la extensión PlatformIO IDE. Si nunca lo hiciste, el tutorial de primeros pasos con PlatformIO explica la instalación en detalle.
Paso 2: configurar platformio.ini
Este portal funciona con Arduino Core. El archivo mínimo para un ESP32-S3 N16R8 es:
| 1 | [env:esp32-s3-n16r8] |
| 2 | platform = espressif32 |
| 3 | board = esp32-s3-devkitc-1 |
| 4 | framework = arduino |
| 5 | monitor_speed = 115200 |
| 6 | upload_speed = 921600 |
| 7 | board_build.arduino.memory_type = qio_opi |
| 8 | build_flags = |
| 9 | -DBOARD_HAS_PSRAM |
| 10 | lib_deps = |
| 11 | bblanchon/ArduinoJson@^7.2.1 |
ArduinoJson construye las respuestas de los endpoints. WebServer y Preferences ya vienen con el core, sin declaración.
Paso 3: grabar y abrir el monitor
Conecta la placa y grábala. En un ESP32-S3 con dos puertos USB, usa el que está marcado como UART:
| 1 | pio run -t upload |
| 2 | pio device monitor -b 115200 |
En el registro de arranque, busca la confirmación de que el punto de acceso se inició y el servidor escucha en el puerto 80.
Paso 4: conectar el teléfono
Abre la lista de redes Wi-Fi del teléfono. La red de la placa aparece con el nombre definido en el firmware. Conéctate a ella.
El teléfono avisará que esa red no tiene Internet; es normal, porque en este momento la placa todavía no está conectada a ningún sitio. Elige mantener la conexión.
Paso 5: abrir el portal y escanear
Abre esta dirección en el navegador del teléfono:
| 1 | http://192.168.4.1 |
Pulsa Scan. El escaneo tarda unos segundos y la lista muestra cada red con su señal en dBm y su canal. Las redes por debajo de -80 dBm tienden a conectar y desconectarse; prefiere las más fuertes.
Si tu red no aparece ni en el segundo intento, usa el campo manual de SSID: las redes ocultas lo necesitan y el campo existe para ese caso.
Paso 6: guardar y acompañar
Elige la red, escribe la contraseña y guarda. La confirmación aparece de inmediato y la página vuelve sola al portal después de ocho segundos.
Si conectó, el panel muestra la dirección que la placa recibió en tu red. Si falló, muestra el motivo y el punto de acceso sigue activo para corregirlo sin cable.
Recuerda que ESP32 no se conecta a redes de 5 GHz. Si la tuya tiene el sufijo 5G, usa la de 2,4 GHz.
Paso 7: confirmar desde la red principal
Desconecta el teléfono del punto de acceso y vuelve a tu red normal. Abre la dirección que mostró el panel. Si el portal responde allí, el aprovisionamiento terminó: la placa está en tu red y sigue accesible.
Errores comunes y cómo resolverlos
La red no aparece en el escaneo. Confirma que es de 2,4 GHz. Si está oculta, usa el SSID manual. Si está lejos, acerca la placa y escanea de nuevo; el tiempo por canal favorece las redes cercanas.
Guardé y la página no respondió. Es el síntoma clásico de un intento bloqueante. Confirma que el handler responde antes de probar la conexión, como se muestra arriba.
Dice que guardó, pero no conecta. Revisa `last_wifi_error` en `/api/status`. Una contraseña incorrecta, WPA3-only y el filtro MAC del router producen el mismo síntoma con mensajes diferentes.
El punto de acceso desaparece cuando falla la conexión. No debería. Verifica que el código no esté desactivando el modo AP al tratar el error; sin él, el técnico pierde el camino de regreso.
Perdí las credenciales y quiero empezar de nuevo. Llama al endpoint de restablecimiento de configuración. Borra el contenido de la NVS y devuelve la placa a su estado lógico de fábrica.
