Objetivo: montar un servidor web en un ESP32 (30 pines / ESP-WROOM-32) que sirva una página HTML y permita:
- Ver lecturas de sensores desde el navegador.
- Controlar salidas (GPIO) (por ejemplo, encender/apagar un LED o un relé).
La idea base es separar en dos capas:
- UI (navegador): una página HTML con JavaScript.
- API (ESP32): endpoints HTTP que devuelven/reciben datos en JSON.
1) Qué vas a construir (arquitectura)
Componentes
- ESP32 conectado a tu Wi-Fi.
- Servidor HTTP escuchando en el puerto 80.
- Página web servida por el ESP32.
- API REST:
GET /api/sensors→ devuelve lecturas en JSON.POST /api/gpio→ recibe comandos y cambia pines.
Por qué esta arquitectura
- Es simple para empezar.
- Es escalable: cambiar sensores o añadir nuevos endpoints no obliga a rehacer la web.
2) Material y requisitos
Hardware mínimo
- 1× ESP32 DevKit V1 (30 pines)
- 1× LED (o relé) + resistencia si procede
- 1× sensor (para el ejemplo: lectura analógica por ADC1, p. ej. LDR)
Recomendación: usa ADC1 (GPIO 32–39) si vas a leer analógico con Wi-Fi activo.
Software
- Arduino IDE
- Soporte de placas ESP32 en Arduino IDE
- Librerías:
ESPAsyncWebServerAsyncTCPArduinoJson
3) Paso a paso en Arduino IDE
Paso 3.1 — Instalar soporte ESP32
- En Arduino IDE, abre Archivo → Preferencias.
- En Gestor de URLs Adicionales de Tarjetas, añade la URL del paquete de ESP32.
- Ve a Herramientas → Placa → Gestor de tarjetas.
- Busca ESP32 e instala el paquete.
Paso 3.2 — Instalar librerías
- Abre Programa → Incluir librería → Gestionar bibliotecas.
- Instala:
- ESPAsyncWebServer
- AsyncTCP
- ArduinoJson
Nota: si Arduino IDE no encuentra ESPAsyncWebServer desde el gestor, instálala desde el repositorio oficial de la librería (ZIP) e importa con Añadir biblioteca .ZIP.
Paso 3.3 — Seleccionar placa y puerto
- Conecta el ESP32 por USB.
- En Herramientas → Placa, selecciona tu modelo (por ejemplo DOIT ESP32 DEVKIT V1).
- En Herramientas → Puerto, selecciona el COM correspondiente.
4) Definir los endpoints (API)
Antes de programar, define qué datos moverás.
Endpoint 1: Sensores
- GET
/api/sensors - Respuesta JSON ejemplo:
{
"adc": 1234,
"uptime": 456789
}
Endpoint 2: Control GPIO
- POST
/api/gpio - Cuerpo JSON ejemplo:
{
"pin": 2,
"state": 1
}
- Respuesta JSON:
{ "ok": true }
5) Código completo (ESP32 + Web)
Este ejemplo:
- Conecta el ESP32 a tu Wi-Fi.
- Publica una página HTML.
- Expone
/api/sensorsy/api/gpio. - La web actualiza lecturas cada 1 segundo.
Ajusta
WIFI_SSIDyWIFI_PASS.
#include <WiFi.h>
#include <ESPmDNS.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
// ====== WiFi ======
static const char* WIFI_SSID = "TU_WIFI";
static const char* WIFI_PASS = "TU_PASSWORD";
// ====== Pines ======
static const int PIN_LED = 2; // LED/relé (salida)
static const int PIN_ADC = 34; // ADC1 recomendado (GPIO34 solo entrada)
// ====== Servidor ======
AsyncWebServer server(80);
// HTML embebido (luego podrás migrarlo a LittleFS)
static const char INDEX_HTML[] PROGMEM = R"HTML(
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Panel IoT ESP32</title>
<style>
body { font-family: system-ui, Arial; margin: 16px; }
.card { border: 1px solid #ddd; border-radius: 10px; padding: 14px; max-width: 520px; }
button { padding: 10px 12px; margin-right: 8px; cursor: pointer; }
code { background: #f6f6f6; padding: 2px 6px; border-radius: 6px; }
.row { margin: 10px 0; }
</style>
</head>
<body>
<h1>Panel IoT (ESP32)</h1>
<div class="card">
<h2>Sensores</h2>
<div class="row">ADC: <code id="adc">-</code></div>
<div class="row">Uptime (ms): <code id="uptime">-</code></div>
<h2>Control</h2>
<button onclick="setGpio(2,1)">LED ON</button>
<button onclick="setGpio(2,0)">LED OFF</button>
<p style="margin-top:12px;">
Actualiza cada 1s usando <code>fetch('/api/sensors')</code>.
</p>
</div>
<script>
async function refreshSensors(){
try{
const res = await fetch('/api/sensors', { cache: 'no-store' });
const data = await res.json();
document.getElementById('adc').textContent = data.adc;
document.getElementById('uptime').textContent = data.uptime;
}catch(e){
console.log('Error leyendo sensores', e);
}
}
async function setGpio(pin, state){
try{
const res = await fetch('/api/gpio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin, state })
});
const data = await res.json();
console.log('GPIO', data);
refreshSensors();
}catch(e){
console.log('Error controlando GPIO', e);
}
}
setInterval(refreshSensors, 1000);
refreshSensors();
</script>
</body>
</html>
)HTML";
static String jsonOK(bool ok) {
StaticJsonDocument<64> doc;
doc["ok"] = ok;
String out;
serializeJson(doc, out);
return out;
}
void setupRoutes() {
// Página principal
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send_P(200, "text/html; charset=utf-8", INDEX_HTML);
});
// API: sensores
server.on("/api/sensors", HTTP_GET, [](AsyncWebServerRequest* request) {
const int adc = analogRead(PIN_ADC);
const uint32_t uptime = millis();
StaticJsonDocument<256> doc;
doc["adc"] = adc;
doc["uptime"] = uptime;
String out;
serializeJson(doc, out);
request->send(200, "application/json; charset=utf-8", out);
});
// API: control GPIO (POST JSON)
server.on("/api/gpio", HTTP_POST,
[](AsyncWebServerRequest* request) {},
nullptr,
[](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
StaticJsonDocument<128> doc;
DeserializationError err = deserializeJson(doc, data, len);
if (err) {
request->send(400, "application/json; charset=utf-8", jsonOK(false));
return;
}
int pin = doc["pin"] | -1;
int state = doc["state"] | 0;
if (pin < 0) {
request->send(400, "application/json; charset=utf-8", jsonOK(false));
return;
}
// Seguridad: limita pines controlables
if (pin == PIN_LED) {
digitalWrite(PIN_LED, state ? HIGH : LOW);
request->send(200, "application/json; charset=utf-8", jsonOK(true));
} else {
request->send(403, "application/json; charset=utf-8", jsonOK(false));
}
}
);
server.onNotFound([](AsyncWebServerRequest* request) {
request->send(404, "text/plain; charset=utf-8", "404 - Not Found");
});
}
void setup() {
Serial.begin(115200);
delay(200);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW);
// ADC (12 bits: 0..4095)
analogReadResolution(12);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Conectando a WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(300);
Serial.print(".");
}
Serial.println();
Serial.print("IP: ");
Serial.println(WiFi.localIP());
// mDNS (si tu red lo soporta)
if (MDNS.begin("esp32")) {
Serial.println("mDNS activo: http://esp32.local");
}
setupRoutes();
server.begin();
Serial.println("Servidor HTTP iniciado");
}
void loop() {
// AsyncWebServer no requiere loop activo
}
6) Cómo probarlo
- Sube el sketch al ESP32.
- Abre el Monitor Serie a 115200.
- Copia la IP que aparece (por ejemplo
192.168.1.50). - Entra desde tu móvil o PC a:
http://192.168.1.50/. - Prueba:
- Ver cómo cambian ADC y uptime.
- Pulsar LED ON y LED OFF.
7) Ajustes típicos y errores comunes
7.1 Si el LED no responde
- No todas las placas usan GPIO2 como LED integrado.
- Cambia
PIN_LEDa otro pin (por ejemplo 4, 5, 18, 19, 21, 22, 23) y conecta un LED externo.
7.2 Si el ADC da valores raros
- Asegúrate de usar ADC1 (GPIO 32–39).
- GPIO34/35 son solo entrada, correcto para lectura.
- Revisa el divisor de tensión si usas LDR.
7.3 Si no abre la web
- Comprueba que el móvil/PC está en la misma red Wi-Fi.
- Revisa firewall del router (raro en LAN, pero posible).
8) Evolución del proyecto (siguiente nivel)
Cuando esto esté estable, los siguientes pasos naturales son:
- Añadir sensores reales (DHT22, BME280, DS18B20) y ampliar el JSON:
{"temp":..., "hum":..., "adc":..., "uptime":...}
- Servir archivos desde LittleFS
- Mover HTML/JS/CSS a ficheros para una web más profesional.
- Tiempo real con WebSocket
- En lugar de
setInterval(fetch...), enviar datos “push” al navegador.
- En lugar de
- Persistencia de configuración
- Guardar SSID/clave o parámetros con
Preferenceso un JSON en LittleFS.
- Guardar SSID/clave o parámetros con
- Seguridad básica
- Autenticación (por ejemplo Basic Auth) y listas blancas de pines.
- Evitar exponer el ESP32 a Internet sin VPN/proxy seguro.
9) Snippet para adaptar a tu blog (infogonzalez)
Entradilla sugerida:
En esta guía voy a montar un panel web minimalista alojado en un ESP32, capaz de mostrar lecturas de sensores y controlar GPIO desde cualq