CodeNaEs

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?

  1. ESP32 conectado a HiveMQ Cloud con SSL
  2. Control de 2 LEDs remotamente
  3. Interfaz web moderna con chat en tiempo real
  4. Autenticación segura con usuario y contraseña
  5. Monitor de actividad en tiempo real

Materiales necesarios

Hardware:

  1. ESP32 (cualquier modelo)
  2. 2 LEDs (opcional para LED externo)
  3. Cables jumper
  4. Protoboard

Software:

  1. Arduino IDE
  2. Cuenta HiveMQ Cloud (gratuita)
  3. Navegador web moderno

Configuración del entorno Arduino IDE

Paso 1: Instalar el soporte para ESP32

  1. Abre Arduino IDE
  2. Ve a Archivo → Preferencias
  3. En "URLs adicionales del Gestor de Tarjetas", agrega:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  1. Ve a Herramientas → Placa → Gestor de tarjetas
  2. 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:

  1. WiFi (incluida con ESP32)
  2. Maneja la conexión inalámbrica
  3. No requiere instalación adicional
  4. PubSubClient por Nick O'Leary
  5. Biblioteca MQTT para Arduino
  6. Busca "PubSubClient" e instala la versión más reciente
  7. WiFiClientSecure (incluida con ESP32)
  8. Maneja conexiones SSL/TLS
  9. Incluida automáticamente

Paso 3: Configurar la placa

  1. Selecciona tu placa ESP32:
  2. Herramientas → Placa → ESP32 Arduino → ESP32 Dev Module
  3. Configura el puerto:
  4. Herramientas → Puerto → COMx (Windows) o /dev/ttyUSBx (Linux/Mac)
  5. Ajusta la velocidad:
  6. 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

  1. Ve a HiveMQ Cloud
  2. Crea una cuenta gratuita
  3. Crea un nuevo cluster

Paso 2: Configurar credenciales

  1. En tu cluster, ve a "Access Management"
  2. Crea un nuevo usuario y contraseña
  3. Anota los datos de conexión:
  4. Cluster URL: abc123def.s1.eu.hivemq.cloud
  5. Puerto SSL: 8883
  6. Usuario: tu_usuario
  7. Contraseña: tu_contraseña

Paso 3: Probar conexión

  1. Ve a "Try out" en tu cluster
  2. Usa el WebSocket Client
  3. Conecta con tus credenciales para verificar

Temas MQTT utilizados:

  1. esp32/led1 - Control del LED 1
  2. esp32/led2 - Control del LED 2
  3. esp32/status - Estado del sistema
  4. esp32/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>
Leds Esp32 Mqtt Hivemq Pagina

Publicado el 10 de septiembre de 2025