Control de LEDs ESP32 con MQTT y HiveMQ: Guía Completa con Interfaz Web
¿Quieres controlar dispositivos desde cualquier lugar del mundo? En este tutorial aprenderás a crear un sistema completo de control de LEDs usando ESP32, MQTT con SSL y una interfaz web moderna. Al final tendrás un panel de control profesional que funciona desde cualquier navegador.
¿Qué vamos a construir?
- ESP32 conectado a HiveMQ Cloud con SSL
- Control de 2 LEDs remotamente
- Interfaz web moderna con chat en tiempo real
- Autenticación segura con usuario y contraseña
- Monitor de actividad en tiempo real
Materiales necesarios
Hardware:
- ESP32 (cualquier modelo)
- 2 LEDs (opcional para LED externo)
- Cables jumper
- Protoboard
Software:
- Arduino IDE
- Cuenta HiveMQ Cloud (gratuita)
- Navegador web moderno
Configuración del entorno Arduino IDE
Paso 1: Instalar el soporte para ESP32
- Abre Arduino IDE
- Ve a Archivo → Preferencias
- En "URLs adicionales del Gestor de Tarjetas", agrega:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
- Ve a Herramientas → Placa → Gestor de tarjetas
- Busca "ESP32" e instala "ESP32 by Espressif Systems"
Paso 2: Instalar las librerías necesarias
Ve a Herramientas → Administrar bibliotecas e instala:
Librerías requeridas:
- WiFi (incluida con ESP32)
- Maneja la conexión inalámbrica
- No requiere instalación adicional
- PubSubClient por Nick O'Leary
- Biblioteca MQTT para Arduino
- Busca "PubSubClient" e instala la versión más reciente
- WiFiClientSecure (incluida con ESP32)
- Maneja conexiones SSL/TLS
- Incluida automáticamente
Paso 3: Configurar la placa
- Selecciona tu placa ESP32:
- Herramientas → Placa → ESP32 Arduino → ESP32 Dev Module
- Configura el puerto:
- Herramientas → Puerto → COMx (Windows) o /dev/ttyUSBx (Linux/Mac)
- Ajusta la velocidad:
- Herramientas → Upload Speed → 115200
Conexiones del circuito
Esquema de conexiones:
ESP32 Componente
Pin 2 → LED → GND
Pin 4 → LED → GND
GND → Ground común
Configuración de HiveMQ Cloud
Paso 1: Crear cuenta gratuita
- Ve a HiveMQ Cloud
- Crea una cuenta gratuita
- Crea un nuevo cluster
Paso 2: Configurar credenciales
- En tu cluster, ve a "Access Management"
- Crea un nuevo usuario y contraseña
- Anota los datos de conexión:
- Cluster URL:
abc123def.s1.eu.hivemq.cloud
- Puerto SSL:
8883
- Usuario: tu_usuario
- Contraseña: tu_contraseña
Paso 3: Probar conexión
- Ve a "Try out" en tu cluster
- Usa el WebSocket Client
- Conecta con tus credenciales para verificar
Temas MQTT utilizados:
esp32/led1
- Control del LED 1esp32/led2
- Control del LED 2esp32/status
- Estado del sistemaesp32/command
- Comandos especiales
Código del ESP32
#include <WiFi.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
const char* ssid = "";
const char* password = "";
const char* mqtt_server = "abc123def.s1.eu.hivemq.cloud";
const int mqtt_port = 8883;
const char* mqtt_client_id = "ESP32_LED_Control";
const char* mqtt_user = "tu_usuario";
const char* mqtt_password = "tu_contraseña";
const char* topic_led1 = "esp32/led1";
const char* topic_led2 = "esp32/led2";
const char* topic_status = "esp32/status";
const char* topic_command = "esp32/command";
const int LED1_PIN = 2;
const int LED2_PIN = 4;
WiFiClientSecure espClient;
PubSubClient client(espClient);
String clientId;
bool led1_state = false;
bool led2_state = false;
void setup() {
Serial.begin(115200);
Serial.println("\n🚀 === ESP32 MQTT SSL LED Control ===");
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
digitalWrite(LED1_PIN, LOW);
digitalWrite(LED2_PIN, LOW);
conectar_wifi();
clientId = "ESP32_" + WiFi.macAddress().substring(9);
clientId.replace(":", "");
Serial.println("🆔 Client ID: " + clientId);
// Configurar MQTT SSL
espClient.setInsecure();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(mensaje_recibido);
}
void conectar_wifi() {
Serial.print("📶 Conectando a WiFi: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n✅ WiFi conectado!");
Serial.print("📶 Señal: ");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
}
void mensaje_recibido(char* topic, byte* payload, unsigned int length) {
String mensaje = "";
for (int i = 0; i < length; i++) {
mensaje += (char)payload[i];
}
Serial.println("\n📨 Mensaje recibido:");
Serial.println(" 📍 Tema: " + String(topic));
Serial.println(" 💬 Mensaje: " + mensaje);
// Control LED 1
if (String(topic) == topic_led1) {
controlar_led(1, mensaje);
}
// Control LED 2
if (String(topic) == topic_led2) {
controlar_led(2, mensaje);
}
if (String(topic) == topic_command) {
if (mensaje == "STATUS") {
enviar_estado();
}
}
}
void controlar_led(int led, String comando) {
int pin = (led == 1) ? LED1_PIN : LED2_PIN;
bool* estado = (led == 1) ? &led1_state : &led2_state;
String nombre = "LED" + String(led);
if (comando == "ON" || comando == "1" || comando == "true") {
digitalWrite(pin, HIGH);
*estado = true;
Serial.println("💡 " + nombre + " ENCENDIDO");
client.publish(topic_status, (nombre + "_ON").c_str());
}
else if (comando == "OFF" || comando == "0" || comando == "false") {
digitalWrite(pin, LOW);
*estado = false;
Serial.println("💡 " + nombre + " APAGADO");
client.publish(topic_status, (nombre + "_OFF").c_str());
}
else if (comando == "TOGGLE") {
*estado = !(*estado);
digitalWrite(pin, *estado);
Serial.println("💡 " + nombre + (*estado ? " ENCENDIDO" : " APAGADO") + " (toggle)");
client.publish(topic_status, (*estado ? nombre + "_ON" : nombre + "_OFF").c_str());
}
}
void enviar_estado() {
String estado = "LED1:" + String(led1_state ? "ON" : "OFF") +
",LED2:" + String(led2_state ? "ON" : "OFF") +
",RAM:" + String(ESP.getFreeHeap()) +
",Uptime:" + String(millis()/1000) + "s";
client.publish(topic_status, estado.c_str());
Serial.println("📊 Estado enviado: " + estado);
}
void conectar_mqtt() {
while (!client.connected()) {
if (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
Serial.println("✅ MQTT SSL conectado!");
client.subscribe(topic_led1);
client.subscribe(topic_led2);
client.subscribe(topic_command);
Serial.println("📡 Suscrito a:");
Serial.println(" • " + String(topic_led1));
Serial.println(" • " + String(topic_led2));
Serial.println(" • " + String(topic_command));
client.publish(topic_status, "ESP32_SSL_CONNECTED");
Serial.println("\n🎯 Comandos disponibles:");
Serial.println(" • ON/OFF - Encender/Apagar LED");
Serial.println(" • TOGGLE - Alternar estado");
Serial.println(" • STATUS - Ver estado actual");
} else {
Serial.print("❌ Error MQTT: ");
Serial.print(client.state());
switch(client.state()) {
case -4: Serial.println(" - Timeout (verificar firewall)"); break;
case -3: Serial.println(" - Conexión perdida"); break;
case -2: Serial.println(" - Conexión fallida"); break;
case -1: Serial.println(" - Desconectado"); break;
case 1: Serial.println(" - Protocolo incorrecto"); break;
case 2: Serial.println(" - ID rechazado"); break;
case 3: Serial.println(" - Servidor no disponible"); break;
case 4: Serial.println(" - Credenciales incorrectas"); break;
case 5: Serial.println(" - No autorizado"); break;
}
Serial.println("🔄 Reintentando en 5 segundos...");
delay(5000);
}
}
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("❌ WiFi desconectado, reconectando...");
conectar_wifi();
}
if (!client.connected()) {
conectar_mqtt();
}
client.loop();
static unsigned long ultimo_heartbeat = 0;
if (millis() - ultimo_heartbeat > 60000) {
ultimo_heartbeat = millis();
String heartbeat = "ALIVE,RAM:" + String(ESP.getFreeHeap()) +
",Uptime:" + String(millis()/1000) + "s";
client.publish(topic_status, heartbeat.c_str());
Serial.println("Estado: " + heartbeat);
}
}
Interfaz Web
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Control ESP32 LEDs - MQTT</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
min-height: 100vh;
}
.panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #4a5568;
font-size: 2.5em;
font-weight: 700;
margin-bottom: 10px;
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background: #e53e3e;
animation: pulse 2s infinite;
margin-right: 8px;
}
.status-indicator.connected {
background: #38a169;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.led-section {
margin-bottom: 40px;
}
.led-title {
font-size: 1.8em;
font-weight: 600;
margin-bottom: 20px;
color: #2d3748;
display: flex;
align-items: center;
gap: 15px;
}
.led-icon {
width: 30px;
height: 30px;
border-radius: 50%;
border: 3px solid #cbd5e0;
transition: all 0.3s ease;
position: relative;
}
.led-icon.on {
background: radial-gradient(circle, #ffd700, #ff6b35);
border-color: #ff6b35;
box-shadow: 0 0 20px rgba(255, 107, 53, 0.6);
}
.led-icon.off {
background: #e2e8f0;
border-color: #cbd5e0;
}
.button-group {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.btn {
flex: 1;
padding: 15px 25px;
border: none;
border-radius: 12px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-on {
background: linear-gradient(45deg, #48bb78, #38a169);
color: white;
}
.btn-on:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(72, 187, 120, 0.4);
}
.btn-off {
background: linear-gradient(45deg, #e53e3e, #c53030);
color: white;
}
.btn-off:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(229, 62, 62, 0.4);
}
.btn-toggle {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
width: 100%;
margin-top: 10px;
}
.btn-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.chat-container {
height: 100%;
display: flex;
flex-direction: column;
}
.chat-header {
text-align: center;
margin-bottom: 20px;
}
.chat-header h2 {
color: #4a5568;
font-size: 1.8em;
margin-bottom: 10px;
}
.connection-config {
margin-bottom: 20px;
padding: 20px;
background: rgba(102, 126, 234, 0.1);
border-radius: 12px;
border-left: 4px solid #667eea;
}
.config-row {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.config-row label {
font-weight: 600;
min-width: 80px;
color: #4a5568;
}
.config-row input {
flex: 1;
padding: 8px 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 0.9em;
}
.config-row input:focus {
outline: none;
border-color: #667eea;
}
.connect-btn {
width: 100%;
padding: 12px;
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.connect-btn:hover {
transform: translateY(-1px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.chat-messages {
flex: 1;
background: #f7fafc;
border-radius: 12px;
padding: 20px;
overflow-y: auto;
max-height: 400px;
border: 1px solid #e2e8f0;
}
.message {
margin-bottom: 12px;
padding: 12px 16px;
border-radius: 10px;
font-size: 0.9em;
line-height: 1.4;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.sent {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
margin-left: 20px;
}
.message.received {
background: #e6fffa;
color: #234e52;
border-left: 4px solid #38b2ac;
}
.message.system {
background: #fef5e7;
color: #744210;
border-left: 4px solid #d69e2e;
}
.message.error {
background: #fed7d7;
color: #742a2a;
border-left: 4px solid #e53e3e;
}
.timestamp {
font-size: 0.8em;
opacity: 0.7;
margin-bottom: 4px;
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
}
.clear-btn {
flex: 1;
padding: 10px;
background: #e2e8f0;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
color: #4a5568;
}
.status-btn {
flex: 1;
padding: 10px;
background: linear-gradient(45deg, #38b2ac, #319795);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
gap: 15px;
padding: 15px;
}
.panel {
padding: 20px;
}
.header h1 {
font-size: 2em;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Panel de Control -->
<div class="panel">
<div class="header">
<h1>🎛️ Control ESP32</h1>
<p><span class="status-indicator" id="connectionStatus"></span>
<span id="statusText">Desconectado</span></p>
</div>
<!-- LED 1 -->
<div class="led-section">
<div class="led-title">
<div class="led-icon off" id="led1Icon"></div>
LED 1 (Pin 2)
</div>
<div class="button-group">
<button class="btn btn-on" onclick="controlLED(1, 'ON')" disabled>
🟢 ENCENDER
</button>
<button class="btn btn-off" onclick="controlLED(1, 'OFF')" disabled>
🔴 APAGAR
</button>
</div>
<button class="btn btn-toggle" onclick="controlLED(1, 'TOGGLE')" disabled>
🔄 ALTERNAR
</button>
</div>
<!-- LED 2 -->
<div class="led-section">
<div class="led-title">
<div class="led-icon off" id="led2Icon"></div>
LED 2 (Pin 4)
</div>
<div class="button-group">
<button class="btn btn-on" onclick="controlLED(2, 'ON')" disabled>
🟢 ENCENDER
</button>
<button class="btn btn-off" onclick="controlLED(2, 'OFF')" disabled>
🔴 APAGAR
</button>
</div>
<button class="btn btn-toggle" onclick="controlLED(2, 'TOGGLE')" disabled>
🔄 ALTERNAR
</button>
</div>
</div>
<!-- Panel de Chat -->
<div class="panel">
<div class="chat-container">
<div class="chat-header">
<h2>💬 Monitor MQTT</h2>
</div>
<!-- Configuración de conexión -->
<div class="connection-config">
<div class="config-row">
<label>Broker:</label>
<input type="text" id="mqttBroker" value="broker.hivemq.com" placeholder="tu-cluster.hivemq.cloud">
</div>
<div class="config-row">
<label>Puerto:</label>
<input type="number" id="mqttPort" value="8884" placeholder="8884">
</div>
<div class="config-row">
<label>Usuario:</label>
<input type="text" id="mqttUser" placeholder="tu_usuario">
</div>
<div class="config-row">
<label>Password:</label>
<input type="password" id="mqttPassword" placeholder="tu_password">
</div>
<button class="connect-btn" onclick="toggleConnection()" id="connectBtn">
🔌 CONECTAR
</button>
</div>
<!-- Mensajes -->
<div class="chat-messages" id="chatMessages">
<div class="message system">
<div class="timestamp">Sistema</div>
🚀 Listo para conectar al broker MQTT
</div>
</div>
<!-- Controles -->
<div class="controls">
<button class="clear-btn" onclick="clearChat()">🗑️ Limpiar</button>
<button class="status-btn" onclick="requestStatus()" disabled id="statusBtn">📊 Estado</button>
</div>
</div>
</div>
</div>
<script>
let client = null;
let isConnected = false;
let led1State = false;
let led2State = false;
const topics = {
led1: "esp32/led1",
led2: "esp32/led2",
status: "esp32/status",
command: "esp32/command"
};
function addMessage(content, type = 'system', topic = '') {
const messagesDiv = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
const timestamp = new Date().toLocaleTimeString();
let topicText = topic ? `[${topic}] ` : '';
messageDiv.innerHTML = `
<div class="timestamp">${timestamp}</div>
${topicText}${content}
`;
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function updateConnectionStatus(connected) {
const statusIndicator = document.getElementById('connectionStatus');
const statusText = document.getElementById('statusText');
const connectBtn = document.getElementById('connectBtn');
const statusBtn = document.getElementById('statusBtn');
const buttons = document.querySelectorAll('.btn');
isConnected = connected;
if (connected) {
statusIndicator.classList.add('connected');
statusText.textContent = 'Conectado SSL';
connectBtn.textContent = '🔌 DESCONECTAR';
statusBtn.disabled = false;
buttons.forEach(btn => btn.disabled = false);
addMessage('✅ Conectado al broker MQTT con SSL', 'system');
} else {
statusIndicator.classList.remove('connected');
statusText.textContent = 'Desconectado';
connectBtn.textContent = '🔌 CONECTAR';
statusBtn.disabled = true;
buttons.forEach(btn => btn.disabled = true);
addMessage('❌ Desconectado del broker MQTT', 'error');
}
}
function toggleConnection() {
if (isConnected) {
disconnect();
} else {
connect();
}
}
function connect() {
const broker = document.getElementById('mqttBroker').value;
const port = parseInt(document.getElementById('mqttPort').value);
const username = document.getElementById('mqttUser').value;
const password = document.getElementById('mqttPassword').value;
if (!broker || !port) {
addMessage('❌ Por favor configura broker y puerto', 'error');
return;
}
addMessage(`🔄 Conectando a ${broker}:${port}...`, 'system');
const clientId = "WebClient_" + Math.random().toString(16).substr(2, 8);
try {
client = new Paho.MQTT.Client(broker, port, "/mqtt", clientId);
client.onConnectionLost = onConnectionLost;
client.onMessageArrived = onMessageArrived;
const options = {
onSuccess: onConnect,
onFailure: onFailure,
useSSL: true,
timeout: 10
};
if (username && password) {
options.userName = username;
options.password = password;
addMessage(`👤 Usando credenciales: ${username}`, 'system');
}
client.connect(options);
} catch (error) {
addMessage(`❌ Error de conexión: ${error.message}`, 'error');
}
}
function disconnect() {
if (client && isConnected) {
client.disconnect();
addMessage('🔌 Desconectando...', 'system');
}
}
function onConnect() {
updateConnectionStatus(true);
client.subscribe(topics.status);
addMessage(`📡 Suscrito a: ${topics.status}`, 'system');
setTimeout(() => {
requestStatus();
}, 1000);
}
function onFailure(responseObject) {
addMessage(`❌ Fallo de conexión: ${responseObject.errorMessage}`, 'error');
updateConnectionStatus(false);
}
function onConnectionLost(responseObject) {
if (responseObject.errorCode !== 0) {
addMessage(`❌ Conexión perdida: ${responseObject.errorMessage}`, 'error');
}
updateConnectionStatus(false);
}
function onMessageArrived(message) {
const topic = message.destinationName;
const content = message.payloadString;
addMessage(content, 'received', topic);
if (content.includes('LED1_ON')) {
updateLEDStatus(1, true);
} else if (content.includes('LED1_OFF')) {
updateLEDStatus(1, false);
}
if (content.includes('LED2_ON')) {
updateLEDStatus(2, true);
} else if (content.includes('LED2_OFF')) {
updateLEDStatus(2, false);
}
}
function controlLED(ledNumber, command) {
if (!isConnected) {
addMessage('❌ No conectado al broker', 'error');
return;
}
const topic = ledNumber === 1 ? topics.led1 : topics.led2;
const message = new Paho.MQTT.Message(command);
message.destinationName = topic;
try {
client.send(message);
addMessage(`${command}`, 'sent', topic);
} catch (error) {
addMessage(`❌ Error enviando mensaje: ${error.message}`, 'error');
}
}
function requestStatus() {
if (!isConnected) return;
const message = new Paho.MQTT.Message("STATUS");
message.destinationName = topics.command;
try {
client.send(message);
addMessage('STATUS', 'sent', topics.command);
} catch (error) {
addMessage(`❌ Error solicitando estado: ${error.message}`, 'error');
}
}
function updateLEDStatus(ledNumber, isOn) {
const icon = document.getElementById(`led${ledNumber}Icon`);
if (isOn) {
icon.classList.remove('off');
icon.classList.add('on');
} else {
icon.classList.remove('on');
icon.classList.add('off');
}
if (ledNumber === 1) {
led1State = isOn;
} else {
led2State = isOn;
}
}
function clearChat() {
const messagesDiv = document.getElementById('chatMessages');
messagesDiv.innerHTML = `
<div class="message system">
<div class="timestamp">Sistema</div>
🧹 Chat limpiado
</div>
`;
}
window.addEventListener('load', () => {
addMessage('🌐 Página cargada. Configura la conexión MQTT.', 'system');
});
document.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && e.target.tagName === 'INPUT') {
if (!isConnected) {
connect();
}
}
});
</script>
</body>
</html>