ESP32 y Arduino

WS2812B MQTT: controla una tira LED direccionable con comandos JSON

Controla una tira LED direccionable WS2812B desde un ESP32 con comandos MQTT en JSON para el color, el brillo y los efectos, y publica el estado retenido para que todos los dashboards estén sincronizados.

Tira LED WS2812B de colores controlada por un ESP32 en un proyecto de iluminación WS2812B MQTT
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Un controlador WS2812B MQTT te permite cambiar el color, el brillo y la animación de una tira LED direccionable desde un móvil, un script, Node-RED o un hub domótico. En este tutorial crearás un firmware para ESP32 que acepta comandos JSON, ejecuta efectos sin bloqueos y publica su estado como retained message. Todo se prueba con broker.connectmq.com, el servidor MQTT público gratuito de ConnectMQ, y simples comandos mosquitto_pub.

Formato de comandos y topics

Las tiras direccionables tienen más estado que un relé: encendido, color, brillo y efecto. Enviar cada uno en un topic distinto se complica enseguida, así que este proyecto usa un topic de comandos JSON y un topic de estado JSON. Todos los campos de un comando son opcionales, lo que significa que un cliente puede cambiar solo el brillo sin conocer el color actual.

Topic Dirección Retenido Ejemplo
connectmq/demo/<id>/light/set De la app al ESP32 No {"color":[255,80,0],"brightness":120}
connectmq/demo/<id>/light/state Del ESP32 a las apps Sí {"state":"ON","color":[255,80,0],"brightness":120,"effect":"solid"}
connectmq/demo/<id>/status Del ESP32 a las apps Sí online / offline

Campos admitidos:

Campo Tipo Significado
state "ON" / "OFF" Enciende o apaga la tira sin olvidar el color
color [r,g,b], de 0 a 255 cada uno Color base para los efectos solid y breathe
brightness De 0 a 255 Brillo global, limitado por un tope en el firmware
effect solid, rainbow, breathe, chase Modo de animación
speed De 1 a 100 Velocidad de la animación

Esta estructura se parece a la que usan muchas herramientas domóticas para las luces JSON, así que puedes adaptarla a una instalación existente con poco esfuerzo. Si quieres profundizar en los patrones de análisis, la guía de ArduinoJson con MQTT en el ESP32 entra en detalle en el dimensionado y la validación de documentos.

Hardware: conexiones y alimentación bien hechas

La mayoría de los proyectos LED fallidos fallan por la alimentación, no por el código. Un LED WS2812B puede consumir una corriente considerable en blanco total, y unos pocos metros de tira superan fácilmente lo que puede suministrar un puerto USB o el regulador del ESP32.

Necesitas:

  • Placa de desarrollo ESP32.
  • Tira WS2812B (este tutorial asume 60 LED; ajusta NUM_LEDS a la tuya).
  • Una fuente de alimentación de 5 V dimensionada para tu tira con margen.
  • Un condensador de 1000 µF entre los 5 V y GND de la tira, en la entrada.
  • Una resistencia de 330 Ω en serie con la línea de datos.
  • Opcional pero recomendable: un 74AHCT125 o un adaptador de nivel similar para la señal de datos.
Cable de la tira Conectar a
5V +5 V de la fuente
GND GND de la fuente y GND del ESP32
DIN GPIO 5 del ESP32 a través de 330 Ω (y del adaptador de nivel si lo usas)

El ESP32 entrega lógica de 3,3 V, mientras que el WS2812B espera un nivel alto cercano a 0,7 × VDD. Muchas tiras funcionan sin adaptador de nivel, sobre todo con cables de datos cortos, pero los primeros píxeles parpadeando o los colores aleatorios son el síntoma clásico de una señal al límite. En tiras largas, inyecta alimentación en ambos extremos para que los LED más lejanos no se vuelvan naranjas a medida que cae la tensión a lo largo del cobre.

El firmware también limita el brillo con MAX_BRIGHTNESS. Es un seguro barato: aunque alguien envíe "brightness":255 en blanco, la tira se mantiene dentro de lo que tu fuente puede soportar.

Librerías

Instala desde el Gestor de librerías de Arduino:

  • PubSubClient de Nick O'Leary.
  • Adafruit NeoPixel de Adafruit, compatible con WS2812B en el ESP32.
  • ArduinoJson versión 7 de Benoit Blanchon.

El firmware ESP32 WS2812B MQTT

#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_NeoPixel.h>
#include <ArduinoJson.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* DEVICE_ID     = "leds-92be0f";
const String BASE = String("connectmq/demo/") + DEVICE_ID;

#define LED_PIN 5
#define NUM_LEDS 60
const uint8_t MAX_BRIGHTNESS = 150;   // protege tu fuente de alimentación

Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
WiFiClient net;
PubSubClient mqtt(net);

bool powerOn = true;
uint8_t r = 255, g = 120, b = 20;
uint8_t brightness = 100;
uint8_t speed = 50;
String effect = "solid";
bool dirty = true;
unsigned long lastFrame = 0, lastAttempt = 0;
uint16_t frame = 0;

void publishState() {
  JsonDocument doc;
  doc["state"] = powerOn ? "ON" : "OFF";
  JsonArray c = doc["color"].to<JsonArray>();
  c.add(r); c.add(g); c.add(b);
  doc["brightness"] = brightness;
  doc["effect"] = effect;
  doc["speed"] = speed;
  char out[160];
  size_t n = serializeJson(doc, out, sizeof(out));
  String topic = BASE + "/light/state";
  mqtt.publish(topic.c_str(), (const uint8_t*)out, n, true);
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  JsonDocument doc;
  if (deserializeJson(doc, payload, length)) {
    Serial.println("Ignoring invalid JSON");
    return;
  }
  if (doc["state"].is<const char*>()) {
    powerOn = strcmp(doc["state"].as<const char*>(), "OFF") != 0;
  }
  if (doc["color"].is<JsonArray>() && doc["color"].size() == 3) {
    r = constrain(doc["color"][0].as<int>(), 0, 255);
    g = constrain(doc["color"][1].as<int>(), 0, 255);
    b = constrain(doc["color"][2].as<int>(), 0, 255);
  }
  if (doc["brightness"].is<int>()) brightness = constrain(doc["brightness"].as<int>(), 0, 255);
  if (doc["speed"].is<int>()) speed = constrain(doc["speed"].as<int>(), 1, 100);
  if (doc["effect"].is<const char*>()) {
    String e = doc["effect"].as<const char*>();
    if (e == "solid" || e == "rainbow" || e == "breathe" || e == "chase") effect = e;
  }
  dirty = true;
  publishState();
}

void render() {
  uint8_t level = map(brightness, 0, 255, 0, MAX_BRIGHTNESS);
  if (!powerOn) {
    strip.clear();
    strip.show();
    return;
  }
  strip.setBrightness(level);
  if (effect == "solid") {
    strip.fill(strip.Color(r, g, b));
  } else if (effect == "rainbow") {
    strip.rainbow(frame * 256);
  } else if (effect == "breathe") {
    float k = (sin(frame * 0.05f) + 1.0f) / 2.0f;   // 0..1
    strip.fill(strip.Color(r * k, g * k, b * k));
  } else if (effect == "chase") {
    strip.clear();
    for (int i = frame % 3; i < NUM_LEDS; i += 3) strip.setPixelColor(i, strip.Color(r, g, b));
  }
  strip.show();
}

bool connectMqtt() {
  String status = BASE + "/status";
  if (!mqtt.connect(DEVICE_ID, nullptr, nullptr, status.c_str(), 1, true, "offline")) {
    Serial.printf("MQTT failed, state=%d\n", mqtt.state());
    return false;
  }
  mqtt.publish(status.c_str(), "online", true);
  mqtt.subscribe((BASE + "/light/set").c_str(), 1);
  publishState();
  return true;
}

void setup() {
  Serial.begin(115200);
  strip.begin();
  strip.clear();
  strip.show();
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);                 // espacio para comandos JSON más grandes
  mqtt.setSocketTimeout(5);
}

void loop() {
  // Los efectos animados se redibujan con un temporizador; el color fijo solo cuando algo cambia
  unsigned long interval = map(speed, 1, 100, 120, 10);
  bool animated = powerOn && effect != "solid";
  if ((animated && millis() - lastFrame >= interval) || dirty) {
    lastFrame = millis();
    frame++;
    render();
    dirty = false;
  }

  if (WiFi.status() != WL_CONNECTED) return;
  if (!mqtt.connected()) {
    if (millis() - lastAttempt > 5000) { lastAttempt = millis(); connectMqtt(); }
    return;
  }
  mqtt.loop();
}

Decisiones de diseño que vale la pena copiar

Fusionar, no sustituir. El callback solo cambia los campos presentes en el comando. Enviar {"brightness":40} atenúa la tira sin tocar el color ni el efecto, que es lo que los usuarios esperan de una luz.

Animación sin bloqueos. Cada efecto calcula un fotograma a partir de un contador y retorna. No hay ningún delay() ni ningún bucle for que dure segundos, así que mqtt.loop() sigue ejecutándose y los comandos se aplican al instante incluso en mitad de un arcoíris.

Los colores fijos no se redibujan constantemente. En algunas plataformas, strip.show() desactiva brevemente las interrupciones mientras envía los datos. Redibujar un color estático cientos de veces por segundo malgasta CPU sin aportar nada.

El estado se publica tras cada comando. Como el topic de estado es retenido, un dashboard que se abra más tarde ve el color actual de inmediato.

Prueba con mosquitto_pub

Observa el dispositivo:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-92be0f -v -t "connectmq/demo/leds-92be0f/#"

Envía algunos comandos:

# Naranja cálido con brillo medio
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-92be0f -t "connectmq/demo/leds-92be0f/light/set" -m '{"color":[255,90,10],"brightness":120,"effect":"solid"}'

# Arcoíris, más rápido
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-92be0f -t "connectmq/demo/leds-92be0f/light/set" -m '{"effect":"rainbow","speed":80}'

# Apagar, conservando el color para la próxima vez
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-92be0f -t "connectmq/demo/leds-92be0f/light/set" -m '{"state":"OFF"}'

A cada comando le sigue un mensaje de estado como este:

{"state":"ON","color":[255,90,10],"brightness":120,"effect":"rainbow","speed":80}

El servidor público de broker.connectmq.com es ideal para este tipo de experimentos rápidos y prototipos, pero cualquiera puede leer lo que hay en él, así que no es para datos de producción. Usa tus propias credenciales antes de colocar la tira en una estantería de casa.

Controla la tira desde un script

Las pruebas por línea de comandos están bien, pero la mayoría de los proyectos LED acaban controlados por algo automático: un horario al atardecer, el estado de una compilación, un timbre. El breve script de Python siguiente usa paho-mqtt 2.x para atenuar la tira hasta un color cálido de tarde y después confirma el cambio leyendo el estado retenido.

import json
import time
import uuid
import paho.mqtt.client as mqtt

BASE = "connectmq/demo/leds-92be0f"

def on_message(client, userdata, msg):
    print("state:", msg.payload.decode())

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"sched-{uuid.uuid4().hex[:8]}")
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.subscribe(f"{BASE}/light/state", qos=1)
client.loop_start()

for level in range(200, 59, -20):
    client.publish(f"{BASE}/light/set", json.dumps({"state": "ON", "color": [255, 120, 40], "brightness": level}), qos=1)
    time.sleep(1)

time.sleep(2)
client.loop_stop()
client.disconnect()

Como cada comando es una actualización parcial y el dispositivo siempre responde con su estado completo, el script no necesita saber nada del modo anterior de la tira. La misma idea funciona desde Node-RED con un nodo mqtt out y un nodo function que construya el objeto JSON, o desde un hub domótico configurado para una luz JSON. Mantén los pasos de atenuación gruesos: un comando por segundo es más que suficiente, y el lugar adecuado para una animación fluida es el motor de efectos del ESP32, no la red.

Lista de comprobación para solucionar problemas

  1. El primer LED muestra un color incorrecto o parpadea. Señal de datos débil. Añade la resistencia de 330 Ω, acorta el cable de datos y usa un adaptador de nivel.
  2. Colores intercambiados (el rojo sale verde). Tu tira usa otro orden de bytes. Prueba NEO_RGB en lugar de NEO_GRB.
  3. El ESP32 se reinicia cuando la tira se pone en blanco. Caída de la fuente. Alimenta la tira con su propia fuente de 5 V, comparte la masa y baja MAX_BRIGHTNESS.
  4. El extremo lejano de la tira se ve amarillo o naranja. Caída de tensión a lo largo de la tira. Inyecta 5 V en ambos extremos o cada par de metros.
  5. Los comandos JSON grandes se ignoran sin avisar. PubSubClient descarta los mensajes mayores que su buffer. El sketch lo sube a 512 bytes; consulta los límites del tamaño del buffer de PubSubClient si envías datos por píxel.
  6. Los efectos se entrecortan cuando MQTT se reconecta. Un intento de conexión bloqueante congela la animación hasta que vence el tiempo de espera del socket. Reduce setSocketTimeout o lleva la red a su propia tarea.

Llévalo a producción

Cuando la tira pase del banco de trabajo a un salón o a un escaparate, conéctala a tu propio broker MQTT gestionado. En el portal de ConnectMQ, crea un broker y usa el usuario, la contraseña y el client ID generados con WiFiClientSecure en el puerto 8883. Un broker MQTT en la nube permite que tu móvil, tu instancia de Node-RED y la tira lleguen al mismo endpoint desde cualquier lugar, sin abrir puertos en tu router.

ConnectMQ también te ofrece un explorador de topics en vivo, para que veas cambiar light/state mientras pruebas efectos, y el cliente MQTT del navegador es una forma rápida de enviar comandos JSON sin un terminal. Si quieres que la tira muestre estados junto a una pequeña pantalla, combínala con una pantalla OLED en ESP32 que muestre mensajes MQTT, o con la conmutación por relé para cortar por completo la alimentación de la tira por la noche.

Empieza con ConnectMQ

Crea un broker Serverless gratuito en app.connectmq.com sin tarjeta de crédito. Obtienes credenciales seguras al momento y puedes controlar tu tira WS2812B desde cualquier lugar en cuestión de minutos.

Preguntas frecuentes

¿Puedo controlar píxeles WS2812B individuales por MQTT?

Sí, enviando un array de colores por píxel, pero mantén los payloads pequeños. En tiras largas suele ser mejor enviar nombres de efectos y parámetros y dejar que el ESP32 genere los píxeles localmente.

¿Por qué mi tira solo parpadea cuando el Wi-Fi está activo?

La librería Adafruit NeoPixel en el ESP32 usa el periférico RMT, que es robusto frente a las interrupciones del Wi-Fi, así que el parpadeo durante la actividad Wi-Fi suele apuntar a ruido en la alimentación o a una señal de datos al límite, más que a un problema de temporización.

¿Cuántos LED puede controlar un ESP32?

El protocolo de datos admite cientos de LED en un solo pin; los límites prácticos son la fuente de alimentación, la tasa de fotogramas y la RAM. Cada LED necesita tres bytes de buffer en modo RGB.

¿Debe ser retenido el estado de la luz?

Sí. Un mensaje de estado retenido permite que cualquier suscriptor nuevo, como un dashboard o una app móvil, muestre el color correcto nada más conectarse.

Artículos relacionados

Ejemplo de ESP-IDF MQTT: crea un cliente esp-mqtt desde cero

Construye un proyecto ESP-IDF desde una carpeta vacía con el componente esp-mqtt: configuración de Wi-Fi en modo estación, el manejador de eventos MQTT, publicación y suscripción, propiedades de MQTT 5 y TLS con el bundle de certificados.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.