Estás instalando un sensor en una bodega, en un invernadero o en un cerro sin señal, y necesitas leer el dato en el celular. No hay router, no hay internet y llevar un notebook con cable USB cada vez no es opción. Para ese escenario el ESP32 tiene una respuesta directa: puede dejar de ser un cliente de tu red WiFi y pasar a crear su propia red, con servidor web incluido.

En esta guía vas a montar, programando con ESP IDF (el framework nativo de Espressif, no el Arduino Core), un ESP32 en modo Access Point que levanta una página web accesible desde cualquier teléfono o notebook que se conecte a él. Al terminar vas a saber crear el proyecto desde la plantilla oficial, entender qué hace cada bloque del código, compilarlo, grabarlo y depurarlo cuando algo falle.

Access Point o estación: dos formas distintas de usar el WiFi

El ESP32 puede trabajar el WiFi de dos maneras y conviene tenerlas claras antes de escribir una línea de código, porque cambian todo el resto del proyecto.

En modo estación (STA) el ESP32 se conecta a un router existente, igual que tu celular. Tiene salida a internet, puede consultar una API, sincronizar la hora por NTP o publicar a un broker MQTT en la nube, pero depende de que esa red exista y de que tú tengas la clave.

En modo Access Point (AP) el ESP32 es el que genera la red. Los demás dispositivos se conectan a él directamente. No hay router de por medio y tampoco hay salida a internet: el ESP32 no enruta el tráfico hacia afuera. Eso significa que en AP puro no vas a poder usar NTP, ni llamar a una API externa, ni validar certificados HTTPS. A cambio, funciona en cualquier parte, sin infraestructura.

Diagrama del ESP32 funcionando como Access Point con dispositivos conectados directamente a él

La regla práctica: si el proyecto vive dentro de una casa con WiFi, usa STA. Si tiene que funcionar en terreno, en una feria, en un taller ajeno o como interfaz de configuración inicial, usa AP.

Estación (STA) Access Point (AP)
Quién crea la red El router El propio ESP32
Necesita infraestructura Sí, router y clave No
Salida a internet No
APIs externas, NTP, MQTT en la nube Disponibles No disponibles
Clientes conectados No aplica Hasta 10 (4 por defecto en el código)
Alcance típico El del router Unos 10 a 30 m según obstáculos
Uso habitual Domótica en casa, telemetría a la nube Terreno, portal de configuración, demos

Vale la pena mencionar que el ESP32 también admite el modo mixto WIFI_MODE_APSTA, en el que actúa como cliente de tu router y como punto de acceso al mismo tiempo. Es más exigente en memoria y complica la depuración, pero es la base de los equipos que siguen ofreciendo su propia red de mantenimiento mientras están conectados a internet.

Lo que vas a necesitar

  • Una placa ESP32 DevKit V1 (o cualquier variante ESP32/ESP32-S3; el ejemplo está probado en un ESP32-S3)
  • Un cable USB de datos para programar y alimentar la placa
  • VS Code con la extensión ESP IDF ya instalada y funcionando
  • Un computador y un celular o notebook para conectarse a la red del ESP32

No se necesita ningún componente externo: todo el proyecto usa solo la radio WiFi que la placa ya trae integrada.

Paso 1: crear el proyecto desde la plantilla de ESP IDF

La extensión de ESP IDF genera el esqueleto completo del proyecto (carpetas, CMakeLists.txt, configuración) sin que tengas que armar nada a mano. Abre la extensión Espressif, despliega el menú Advanced y elige New Project Wizard.

Lo primero que pide es la versión del framework. Selecciona Use ESP IDF v6.X.

Asistente de nuevo proyecto de la extensión ESP IDF en VS Code mostrando la selección de versión del framework

Después, en la lista de proyectos de ejemplo, elige ESP IDF Templates y presiona el botón para crear el proyecto a partir de esa plantilla.

Selección de la plantilla ESP IDF Templates para crear un proyecto nuevo

Se abre una ventana con los campos de configuración. Complétalos así:

  • Project Name: el nombre que quieras para el proyecto.
  • Enter Project Directory: la carpeta donde se guardará todo. Importante: no uses una carpeta sincronizada de Google Drive, OneDrive o Dropbox. La compilación crea y escribe miles de archivos, y sobre una carpeta en la nube el proceso se vuelve lentísimo.
  • ESP IDF Target: el chip de tu placa (esp32, esp32s3, etc.).
  • ESP IDF Board: la configuración de la placa. Para el ESP32-S3 corresponde ESP32-S chip (via builtin USB JTAG).
  • Serial Port: con la placa conectada, el puerto COM que le corresponde.

Ventana del asistente con los campos de nombre, directorio, chip, placa y puerto serial

Al terminar, VS Code muestra una notificación para abrir el proyecto recién creado. Haz clic en Open Project.

Notificación de VS Code ofreciendo abrir el proyecto ESP IDF recién creado

Si la notificación no aparece, no pasa nada: ve a File > Open Folder…, busca la carpeta del proyecto y selecciónala.

La plantilla genera bastantes archivos y subcarpetas. Para esta guía deja todo como está: lo único que vas a modificar es main.c. Ábrelo desde el explorador lateral, dentro de la carpeta main.

Archivo main.c de la plantilla abierto en el editor de VS Code

Paso 2: el código completo

Reemplaza el contenido de main.c por este código. Levanta el Access Point y el servidor HTTP que entrega una página HTML simple.

C
/*  
  Rui Santos & Sara Santos - Random Nerd Tutorials
  https://RandomNerdTutorials.com/esp-idf-esp32-web-server-access-point/
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "esp_mac.h"
#include "sdkconfig.h"

// Replace with your Access Point (AP) SSID and Password
#define MY_ESP_WIFI_SSID      "ESP32_AP"
#define MY_ESP_WIFI_PASS      "password123"

static const char *TAG = "web_server_ap";

// Global AP netif handle
static esp_netif_t *ap_netif;

// HTML web page to serve 
static const char *html_page = 
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<title>ESP-IDF: Web Server (Access Point)</title>"
    "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
    "</head>"
    "<body>"
    "<h1>ESP-IDF: Web Server (Access Point)</h1>"
    "<p>Hello from ESP32!</p>"
    "<p>Connected to ESP32 via Access Point (AP) Mode.</p>"
    "</body>"
    "</html>";

// HTTP GET handler for root "/"
static esp_err_t root_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, html_page, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

// Start the HTTP server
static httpd_handle_t start_web_server(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_handle_t server = NULL;

    if (httpd_start(&server, &config) == ESP_OK) {
        ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);
        httpd_uri_t uri_get = {
            .uri       = "/",
            .method    = HTTP_GET,
            .handler   = root_get_handler,
            .user_ctx  = NULL
        };
        httpd_register_uri_handler(server, &uri_get);
        return server;
    }
    ESP_LOGE(TAG, "Failed to start HTTP server");
    return NULL;
}

// Wi-Fi and IP event handler
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) {
        ESP_LOGI(TAG, "Wi-Fi AP started! SSID: %s", MY_ESP_WIFI_SSID);

        esp_netif_ip_info_t ip_info;
        if (esp_netif_get_ip_info(ap_netif, &ip_info) == ESP_OK) {
            ESP_LOGI(TAG, "AP IP: " IPSTR, IP2STR(&ip_info.ip));
            ESP_LOGI(TAG, "Web server ready → http://" IPSTR "/", IP2STR(&ip_info.ip));
        }
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "Device connected: " MACSTR, MAC2STR(event->mac));
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "Device disconnected: " MACSTR, MAC2STR(event->mac));
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Starting... Free heap: %d bytes", esp_get_free_heap_size());

    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // Initialize TCP/IP stack and event loop
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // Create default Wi-Fi STA interface
    ap_netif = esp_netif_create_default_wifi_ap();

    // Initialize Wi-Fi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // Register event handlers
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler, NULL, NULL));

    // Configure Wi-Fi Access Point (AP) Configuration
    wifi_config_t wifi_config = {
        .ap = {
            .ssid = MY_ESP_WIFI_SSID,
            .ssid_len = strlen(MY_ESP_WIFI_SSID),
            .channel = 1,
            .password = MY_ESP_WIFI_PASS,
            .max_connection = 4,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK,
        },
    };

    // Safety checks for correct SSID/Password to prevent reboots
    if (strlen(MY_ESP_WIFI_SSID) == 0 || strlen(MY_ESP_WIFI_SSID) > 32) {
        ESP_LOGE(TAG, "Invalid SSID length!");
        return;
    }
    if (strlen(MY_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
        ESP_LOGW(TAG, "No password → Open AP (anyone can connect)");
    } else if (strlen(MY_ESP_WIFI_PASS) < 8) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
        wifi_config.ap.password[0] = '\0';
        ESP_LOGW(TAG, "Password too short (<8 chars) → Forced Open AP");
    } else {
        ESP_LOGI(TAG, "Using WPA2-PSK (password OK)");
    }

    // Start Wi-Fi interface in Access Point (AP) Mode
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    // Start web server in AP mode
    httpd_handle_t server = start_web_server();
    if (server) {
        ESP_LOGI(TAG, "Web server initialized. Connect to the Access Point (AP) now!");
    }
}

Paso 3: qué hace cada parte del código

Las librerías

C
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "esp_mac.h"
#include "sdkconfig.h"

Las dos primeras son de la biblioteca estándar de C: stdio.h para imprimir y string.h para manipular cadenas. FreeRTOS.h y task.h traen el sistema operativo de tiempo real sobre el que corre todo en el ESP32. esp_wifi.h configura la radio, esp_event.h maneja los eventos, esp_log.h da el sistema de logging, nvs_flash.h permite guardar datos en memoria no volátil, esp_netif.h aporta la interfaz de red, esp_http_server.h es el servidor HTTP y esp_mac.h sirve para imprimir direcciones MAC.

El nombre y la clave de la red

C
#define MY_ESP_WIFI_SSID      "ESP32_AP"
#define MY_ESP_WIFI_PASS     "password123"

Estos dos valores son el nombre de red (SSID) y la contraseña que verás en el menú WiFi de tu celular. Cámbialos por lo que quieras, pero ojo con la clave: más abajo hay una regla importante sobre su largo.

La etiqueta de log

C
static const char *TAG = "web_server_ap";

Todos los mensajes de depuración van a salir en el monitor serial precedidos por web_server_ap. Usar una etiqueta propia por módulo es lo que después te permite filtrar el log cuando el proyecto crece.

La página que se entrega

C
static const char *html_page = 
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<title>ESP-IDF: Web Server (Access Point)</title>"
    "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
    "</head>"
    "<body>"
    "<h1>ESP-IDF: Web Server (Access Point)</h1>"
    "<p>Hello from ESP32!</p>"
    "<p>Connected to ESP32 via Access Point (AP) Mode.</p>"
    "</body>"
    "</html>";

El HTML va como una cadena de C constante, guardada en flash. Fíjate en las comillas escapadas (\") dentro del meta viewport: son necesarias porque el HTML vive dentro de una cadena de C. Para páginas más grandes esto se vuelve incómodo y conviene pasar los archivos a una partición SPIFFS o LittleFS, pero para una página de prueba resuelve bien.

El manejador de la petición

C
static esp_err_t root_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, html_page, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

Esta función corre cada vez que alguien conectado al AP abre la raíz / en el navegador. httpd_resp_set_type le avisa al navegador que lo que viene es HTML, y httpd_resp_send envía la página. HTTPD_RESP_USE_STRLEN le dice al servidor que calcule solo el largo de la cadena, así no tienes que pasarlo a mano.

Levantar el servidor HTTP

El servidor arranca con la configuración por defecto, que usa el puerto 80:

C
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_handle_t server = NULL;

Luego se registra la ruta: cuando alguien pide /, se ejecuta root_get_handler.

C
if (httpd_start(&server, &config) == ESP_OK) {
    ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);
        
    // Register URI handler
    httpd_uri_t uri_get = {
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = root_get_handler,
        .user_ctx  = NULL
    };
    httpd_register_uri_handler(server, &uri_get);
        
    return server;
}

Si el servidor no logra iniciar, se imprime el error:

C
ESP_LOGE(TAG, "Failed to start HTTP server");

Los eventos de WiFi

C
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) {
        ESP_LOGI(TAG, "Wi-Fi AP started! SSID: %s", MY_ESP_WIFI_SSID);

        esp_netif_ip_info_t ip_info;
        if (esp_netif_get_ip_info(ap_netif, &ip_info) == ESP_OK) {
            ESP_LOGI(TAG, "AP IP: " IPSTR, IP2STR(&ip_info.ip));
            ESP_LOGI(TAG, "Web server ready → http://" IPSTR "/", IP2STR(&ip_info.ip));
        }
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "Device connected: " MACSTR, MAC2STR(event->mac));
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "Device disconnected: " MACSTR, MAC2STR(event->mac));
    }
}

Esta función se ejecuta sola cuando ocurre algo en la radio. WIFI_EVENT_AP_START avisa que el AP quedó operativo y es el momento en que se imprime la IP. WIFI_EVENT_AP_STACONNECTED y WIFI_EVENT_AP_STADISCONNECTED se disparan cuando un dispositivo entra o sale de la red, e imprimen su MAC. Ese par de eventos es útil de verdad: te permite saber cuántos clientes tienes conectados sin adivinar.

app_main

En ESP IDF no existen setup() ni loop(). El punto de entrada es app_main, y es la función que el framework llama al arrancar.

C
void app_main(void)
{
    // your code goes here
}

Lo primero es inicializar la NVS, porque el driver de WiFi guarda ahí sus parámetros de calibración. Si la partición está llena o quedó de una versión anterior del framework, se borra y se vuelve a inicializar:

C
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

Después se levanta la pila TCP/IP y el bucle de eventos por defecto:

C
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());

esp_netif_create_default_wifi_ap() crea la interfaz de red en modo AP. Esta llamada hace más de lo que parece, y es la razón de un detalle que aparece más adelante: además de la interfaz, levanta un servidor DHCP que le asigna direcciones a quien se conecte. Por eso el ESP32 queda en 192.168.4.1 y los clientes reciben 192.168.4.2, .3, etc. Esa subred es el valor por defecto de ESP IDF; se puede cambiar deteniendo el DHCP con esp_netif_dhcps_stop(), fijando otra IP y volviéndolo a iniciar.

C
ap_netif = esp_netif_create_default_wifi_ap();

wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));

Se registra el manejador de eventos para toda la familia WIFI_EVENT:

C
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));

Y se arma la configuración del punto de acceso:

C
wifi_config_t wifi_config = {
    .ap = {
        .ssid = MY_ESP_WIFI_SSID,
        .ssid_len = strlen(MY_ESP_WIFI_SSID),
        .channel = 1,
        .password = MY_ESP_WIFI_PASS,
        .max_connection = 4,
        .authmode = WIFI_AUTH_WPA_WPA2_PSK,
    },
};

Dos campos merecen atención. channel = 1 fija el canal de la banda de 2,4 GHz; en un lugar con muchas redes alrededor conviene probar el 6 o el 11, que son los otros dos canales que no se solapan. Y max_connection = 4 limita los clientes simultáneos: el ESP32 admite hasta 10, pero cada cliente consume RAM, así que subir ese número sin necesidad solo te deja con menos memoria para tu propia aplicación.

La validación que evita un reinicio en bucle

C
if (strlen(MY_ESP_WIFI_SSID) == 0 || strlen(MY_ESP_WIFI_SSID) > 32) {
    ESP_LOGE(TAG, "Invalid SSID length!");
    return;
}
if (strlen(MY_ESP_WIFI_PASS) == 0) {
    wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    ESP_LOGW(TAG, "No password → Open AP (anyone can connect)");
} else if (strlen(MY_ESP_WIFI_PASS) < 8) {
    wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    wifi_config.ap.password[0] = '\0';
    ESP_LOGW(TAG, "Password too short (<8 chars) → Forced Open AP");
} else {
    ESP_LOGI(TAG, "Using WPA2-PSK (password OK)");
}

Este bloque es la parte más valiosa del ejemplo y la que más dolores de cabeza ahorra. WPA2 exige una clave de al menos 8 caracteres. Si le pasas una más corta, el driver de WiFi falla al aplicar la configuración, ESP_ERROR_CHECK aborta y la placa entra en un ciclo de reinicios que desde afuera parece un problema de hardware. En vez de eso, el código detecta el caso y baja la red a abierta (WIFI_AUTH_OPEN) dejando una advertencia en el log. Lo mismo con el SSID: vacío o de más de 32 caracteres, y sale con un error claro.

Finalmente se aplica el modo AP, se arranca la radio y se levanta el servidor:

C
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
C
httpd_handle_t server = start_web_server();
if (server) {
    ESP_LOGI(TAG, "Web Server initialized. Waiting for Wi-Fi connection...");
}

Paso 4: compilar y grabar en la placa

Todo el flujo se maneja desde la barra inferior de VS Code. Si usaste el asistente, lo más probable es que ya esté bien configurado; si no, revisa estas cuatro cosas en orden.

Primero, el método de grabado: haz clic en el ícono correspondiente y elige UART.

Menú de VS Code para seleccionar UART como método de grabado del ESP32

Con la placa conectada, haz clic en el ícono del enchufe y selecciona el puerto COM correcto.

Selección del puerto COM correspondiente a la placa ESP32 en la barra inferior de VS Code

Luego el chip de destino, con el ícono del circuito integrado.

Selección del chip de destino ESP32-S3 en la extensión ESP IDF

Ya con eso configurado, compila con el ícono de la llave inglesa (Build Project).

Botón Build Project de la extensión ESP IDF en VS Code

La primera compilación demora bastante más que las siguientes, porque construye todo el framework desde cero. Al terminar debería aparecer el mensaje de compilación exitosa en la terminal.

Ahora graba el firmware con el botón del rayo (Flash Device). Según la placa, puede que tengas que mantener presionado el botón BOOT para que entre en modo de grabado. Al terminar aparece el aviso Flash Done.

Mensaje de grabado completado (Flash Done) en VS Code tras programar el ESP32

Paso 5: probar el servidor

Abre el monitor serial con la herramienta Monitor Device (el ícono de pantalla). El ESP32 levanta la interfaz en modo AP e imprime su dirección IP. Anótala: es la que vas a escribir en el navegador.

Monitor serial mostrando el arranque del Access Point y la dirección IP del ESP32

Si el monitor no muestra nada, presiona el botón EN (reset) de la placa: es normal cuando el monitor se abre después de que el chip ya arrancó y te perdiste los primeros mensajes.

Ahora conecta tu computador o celular a la red del ESP32: abre el menú WiFi, elige la red ESP32_AP e ingresa la clave (password123 si no la cambiaste).

Menú de WiFi del dispositivo conectándose a la red ESP32_AP

Con la conexión establecida, abre el navegador y escribe la IP: http://192.168.4.1. Deberías ver la página servida por el ESP32.

Navegador mostrando la página web servida por el ESP32 en modo Access Point

Al mismo tiempo, el monitor serial imprime la MAC del dispositivo que se conectó.

Monitor serial imprimiendo la dirección MAC del dispositivo conectado al Access Point

Errores comunes y cómo depurarlos

  • La placa se reinicia en bucle apenas arranca. Casi siempre es la clave de menos de 8 caracteres. El código de esta guía ya lo controla, pero si partiste de otro ejemplo, revisa eso primero.
  • La red aparece pero el navegador no carga nada. Verifica que estás usando http:// y no https://. El servidor de este ejemplo no maneja TLS, y muchos navegadores fuerzan HTTPS por defecto.
  • El celular se conecta y se desconecta solo. Android e iOS detectan que la red no tiene salida a internet y a veces vuelven a los datos móviles. Hay que aceptar el aviso de "mantener conexión" para que no te saque.
  • No aparece nada en el monitor serial. Presiona EN, y confirma que el cable USB es de datos y no solo de carga: es la causa más frecuente y la más fácil de descartar.
  • Failed to start HTTP server. Suele ser falta de memoria. El log de arranque imprime el heap libre; si está muy bajo, revisa qué más estás levantando antes del servidor.

Una nota sobre seguridad antes de dejarlo instalado

El ejemplo trae ESP32_AP y password123 para que puedas probarlo de inmediato, pero eso no debería sobrevivir a la etapa de prototipo. Si el equipo va a quedar funcionando en algún lado, cambia ambos valores: cualquiera que vea un tutorial con estas credenciales por defecto puede conectarse a tu red.

Ten presente además que el servidor entrega la página por HTTP plano, sin cifrado a nivel de aplicación. La protección real la da WPA2 en la capa WiFi, así que dejar la red abierta (que es lo que hace el código automáticamente cuando la clave es muy corta) significa que el tráfico viaja sin ninguna protección. Para un proyecto que solo muestra una lectura ambiental da lo mismo; para uno que acciona un relé conectado a la red eléctrica, no.

Por último, este servidor no tiene autenticación: cualquiera que esté conectado al AP puede abrir la página y, si agregas las rutas de control de las variantes, accionar las salidas. Si eso importa en tu caso, el paso siguiente es agregar una verificación de token en el manejador antes de ejecutar la acción.

Variantes y mejoras

El ejemplo entrega una página estática, pero la estructura ya está lista para crecer. Tres extensiones concretas:

  1. Panel de control de un relé. Registra dos rutas más (/on y /off) con httpd_register_uri_handler y en cada manejador cambia un GPIO con gpio_set_level(). Agrega dos botones a la página HTML apuntando a esas rutas y ya tienes un control de encendido accesible desde el celular, sin router.
  2. Mostrar la lectura de un sensor. Con un DHT22 conectado a un GPIO puedes armar la página en tiempo de ejecución con snprintf() en vez de servir una cadena fija, e inyectar temperatura y humedad en el HTML. Combinado con lo anterior, en la misma página tienes lectura y control.
  3. Portal de configuración WiFi. Es el uso más habitual del modo AP en productos reales: el equipo parte como Access Point, muestra un formulario donde el usuario escribe la red y la clave de su casa, las guarda en NVS y reinicia en modo estación. El nvs_flash_init() que ya está en el código es justamente la pieza que te permite guardar esos datos.

Personalización para Chile

Este proyecto no necesita componentes externos: basta la placa, el cable y, si quieres dejarlo montado, una protoboard. En MechatronicStore puedes conseguir todo lo necesario con despacho local:

  • Placa ESP32 DevKit V1. la opción más común y económica para partir. El código de esta guía funciona tal cual; solo tienes que elegir esp32 en vez de esp32s3 como target del proyecto.
  • Placa ESP32-S3. si quieres seguir el ejemplo exactamente como está escrito, incluido el grabado por USB JTAG integrado que evita el botón BOOT.
  • Cable USB de datos. el que corresponda a tu placa (micro USB en las DevKit V1 clásicas, USB-C en las más nuevas). Asegúrate de que sea de datos.
  • Protoboard 830 puntos. opcional, para dejar la placa firme mientras trabajas y tener espacio para los sensores de las variantes de arriba.

Si el tutorial original menciona una placa de otra marca, cualquier ESP32 del catálogo cumple la misma función: lo que importa es el chip, no el fabricante de la placa. Consulta precios y stock actualizados directamente en el catálogo.

Recursos

Guía inspirada en el tutorial de Random Nerd Tutorials, reescrita y ampliada con la comparación entre modos STA y AP, la explicación del servidor DHCP interno, la sección de errores frecuentes y tres variantes de extensión que el original no cubre. Versión chilena con componentes en stock local en MechatronicStore.