CodeNaEs

Controla LED del ESP32 desde el navegador con WebSocket

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é?

  1. 🐍 Python con websockets y aiohttp para el servidor WebSocket y HTTP.
  2. 📡 ESP32 con la librería WebSocketsClient.
  3. 🌐 Un frontend HTML + JavaScript nativo para controlar el LED.
  4. 📶 Comunicación bidireccional en tiempo real gracias a WebSocket.

El flujo general

  1. El navegador abre una conexión WebSocket al servidor y se identifica como "web".
  2. El ESP32 se conecta por WebSocket y se identifica como "esp32".
  3. Si el ESP32 está conectado, se muestra en la interfaz y se puede controlar el LED.
  4. 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:

  1. 9080: Para servir el archivo HTML.
  2. 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

  1. ✅ El LED del ESP32 se puede prender o apagar desde cualquier navegador en la red local.
  2. 🔁 Si se desconecta el ESP32, la interfaz lo detecta en tiempo real.
  3. 🔄 Soporta múltiples navegadores abiertos simultáneamente.

Posibles mejoras

  1. Añadir autenticación para conexiones WebSocket.
  2. Persistencia del último estado del LED.
  3. Controlar más de un dispositivo (multi-ID).
  4. 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)
Esp32 Led Python Websocket

Publicado el 27 de mayo de 2025