
En este pequeño proyecto se conecta una placa ESP32 a una interfaz web que me permite controlar un LED en tiempo real. Todo se comunica a través de WebSockets, sin necesidad de recargar la página o enviar peticiones HTTP tradicionales.
🧩 ¿Qué tecnologías usé?
- 🐍 Python con
websockets
y aiohttp
para el servidor WebSocket y HTTP. - 📡 ESP32 con la librería
WebSocketsClient
. - 🌐 Un frontend HTML + JavaScript nativo para controlar el LED.
- 📶 Comunicación bidireccional en tiempo real gracias a WebSocket.
El flujo general
- El navegador abre una conexión WebSocket al servidor y se identifica como
"web"
. - El ESP32 se conecta por WebSocket y se identifica como
"esp32"
. - Si el ESP32 está conectado, se muestra en la interfaz y se puede controlar el LED.
- Cada vez que se envía un comando desde la web (
LED_ON
o LED_OFF
), el ESP32 lo recibe y responde con una confirmación.

Backend en Python (Servidor WebSocket + HTTP)
import asyncio
import websockets
from aiohttp import web
web_clients = set()
esp32_clients = set()
last_message = None
puertoWeb = 9080
puertoWs = 9081
async def websocket_handler(websocket):
global last_message
identity = None
try:
identity = await websocket.recv()
print(f"Cliente identificado como: {identity}")
if identity == "web":
web_clients.add(websocket)
# Notificar el estado actual del ESP32 al navegador
estado = "connected" if esp32_clients else "disconnected"
await websocket.send(f"ESP32_STATUS:{estado}")
elif identity == "esp32":
esp32_clients.add(websocket)
print("ESP32 conectado")
# Enviar último comando si existe
if last_message:
await websocket.send(last_message)
for client in web_clients:
await client.send("ESP32_STATUS:connected")
else:
await websocket.send("Identificador no válido")
await websocket.close()
return
async for message in websocket:
print(f"[{identity}] Mensaje: {message}")
if identity == "web":
last_message = message
for client in esp32_clients:
await client.send(message)
elif identity == "esp32":
for client in web_clients:
await client.send(f"ESP32: {message}")
except websockets.exceptions.ConnectionClosed:
print(f"⚠️ Cliente {identity} desconectado inesperadamente")
finally:
if identity == "web":
web_clients.discard(websocket)
print("Navegador desconectado")
elif identity == "esp32":
esp32_clients.discard(websocket)
print("ESP32 desconectado")
for client in web_clients:
await client.send("ESP32_STATUS:disconnected")
# Servir archivo HTML
async def html_handler(request):
return web.FileResponse('index.html')
async def main():
# Servidor WebSocket
ws_server = await websockets.serve(
websocket_handler,
"0.0.0.0",
puertoWs,
ping_interval=5,
ping_timeout=5
)
# Servidor HTTP
app = web.Application()
app.router.add_get("/", html_handler)
app.router.add_static("/", ".")
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", puertoWeb)
await site.start()
print(f"Servidor corriendo en http://localhost:{puertoWeb} y WebSocket en ws://localhost:{puertoWs}")
await asyncio.Future()
asyncio.run(main())
El servidor escucha en dos puertos:
9080
: Para servir el archivo HTML.9081
: Para las conexiones WebSocket.
🔄 Si un cliente Web se conecta, recibe el estado del ESP32 (connected
o disconnected
).
📤 Cuando se envía un mensaje desde la web, se reenvía al ESP32.
📥 El ESP32 responde, y ese mensaje se reenvía al navegador para confirmación.
Interfaz Web (HTML + JavaScript)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Control LED ESP32</title>
<style>
body {
text-align: center;
font-family: sans-serif;
margin-top: 40px;
background-color: #f9f9f9;
}
button {
margin: 0 10px;
padding: 10px 20px;
font-size: 16px;
}
#estado {
margin-top: 10px;
font-weight: bold;
color: darkblue;
}
#log {
margin: 20px auto;
width: 90%;
max-width: 600px;
height: 250px;
overflow-y: auto;
background: #fff;
border: 1px solid #ccc;
border-radius: 6px;
padding: 10px;
text-align: left;
font-family: monospace;
font-size: 14px;
}
#log p {
margin: 5px 0;
}
.web {
color: blue;
}
.esp32 {
color: green;
}
.alerta {
color: red;
}
</style>
</head>
<body>
<h1>Control de LED vía WebSocket</h1>
<button id="btnOn" onclick="enviar('LED_ON')">Encender LED</button>
<button id="btnOff" onclick="enviar('LED_OFF')">Apagar LED</button>
<p id="estado">Estado del ESP32: desconocido</p>
<div id="log"></div>
<script>
let ws;
let confirmTimeout;
function crearWebSocket() {
ws = new WebSocket("ws://" + location.hostname + ":9081");
ws.onopen = () => {
ws.send("web");
agregarLinea("Web: ✅ Conectado al servidor WebSocket", "web");
};
ws.onmessage = (event) => {
const data = event.data;
if (data.startsWith("ESP32_STATUS:")) {
const status = data.split(":")[1];
document.getElementById("estado").textContent =
status === "connected"
? "🟢 ESP32 Conectado"
: "🔴 ESP32 Desconectado";
agregarLinea(
`ESP32-01: ${
status === "connected" ? "Conectado" : "Desconectado"
}`,
"esp32"
);
return;
}
if (data.startsWith("ESP32: ")) {
agregarLinea(
"ESP32-01: 💡 " + data.replace("ESP32: ", ""),
"esp32"
);
clearTimeout(confirmTimeout);
return;
}
agregarLinea("Mensaje: " + data);
};
ws.onerror = (err) => {
console.error("❌ Error de WebSocket:", err);
agregarLinea("Web: ❌ Error de WebSocket", "alerta");
};
ws.onclose = () => {
agregarLinea("Web: ⚠️ Conexión WebSocket cerrada", "alerta");
setTimeout(() => {
agregarLinea("Web: 🔄 Reintentando conexión...", "alerta");
crearWebSocket();
}, 3000);
};
}
function enviar(msg) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(msg);
agregarLinea(`Web: ✅ Comando "${msg}" enviado`, "web");
clearTimeout(confirmTimeout);
confirmTimeout = setTimeout(() => {
agregarLinea(
"ESP32-01: ❌ Sin respuesta del dispositivo",
"alerta"
);
}, 5000);
} else {
agregarLinea("Web: ❌ WebSocket no conectado", "alerta");
}
}
function agregarLinea(texto, clase = "") {
const log = document.getElementById("log");
const linea = document.createElement("p");
linea.textContent = texto;
if (clase) linea.classList.add(clase);
log.appendChild(linea);
log.scrollTop = log.scrollHeight;
}
crearWebSocket();
</script>
</body>
</html>
🖱️ Dos botones para enviar comandos: Encender LED y Apagar LED.
📡 Usa WebSocket
en JS para conectarse automáticamente al servidor.
🧠 Cada acción queda registrada en un pequeño "log de eventos", y si el ESP32 no responde en 5 segundos, se muestra una alerta de "sin respuesta".
Código del ESP32 (Arduino/C++)
#include <WiFi.h>
#include <WebSocketsClient.h>
const char* ssid = "xxxxxxxxxxxxxxxxxxxx";
const char* password = "xxxxxxxxxxxxxxxxxxxx";
#define LED_PIN 2
WebSocketsClient webSocket;
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_CONNECTED:
Serial.println("✅ Conectado al servidor WebSocket");
webSocket.sendTXT("esp32"); // Identificación
break;
case WStype_TEXT:
Serial.printf("Mensaje recibido: %s\n", payload);
if (strcmp((char*)payload, "LED_ON") == 0) {
digitalWrite(LED_PIN, HIGH);
webSocket.sendTXT("LED encendido");
} else if (strcmp((char*)payload, "LED_OFF") == 0) {
digitalWrite(LED_PIN, LOW);
webSocket.sendTXT("LED apagado");
}
break;
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi conectado");
webSocket.begin("La ip local donde esta el servidor", 9081, "/");
webSocket.onEvent(webSocketEvent);
webSocket.setReconnectInterval(5000);
}
void loop() {
webSocket.loop();
}
🛠️ El ESP32 se conecta a tu red WiFi y luego al WebSocket del servidor.
📨 Se identifica como "esp32"
al conectarse.
💡 Al recibir "LED_ON"
o "LED_OFF"
, prende o apaga el LED del pin 2 y responde al navegador para confirmar que ejecutó la acción.
Resultado
- ✅ El LED del ESP32 se puede prender o apagar desde cualquier navegador en la red local.
- 🔁 Si se desconecta el ESP32, la interfaz lo detecta en tiempo real.
- 🔄 Soporta múltiples navegadores abiertos simultáneamente.
Posibles mejoras
- Añadir autenticación para conexiones WebSocket.
- Persistencia del último estado del LED.
- Controlar más de un dispositivo (multi-ID).
- Usar HTTPS y WSS para mayor seguridad.
Estructura del proyecto
📁 proyecto_control_led/
├── 📄 server.py # Código principal Python: servidor WebSocket + HTTP
├── 📄 index.html # Interfaz web para controlar el LED
└── 📄 main.ino (ESP32)