ESP32 and Arduino

WS2812B MQTT: Control an Addressable LED Strip with JSON Commands

Drive a WS2812B addressable LED strip from an ESP32 with JSON MQTT commands for color, brightness and effects, and publish retained state so every dashboard stays in sync.

Colorful WS2812B LED strip driven by an ESP32 in a ws2812b mqtt lighting project
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

A WS2812B MQTT controller lets you change the color, brightness and animation of an addressable LED strip from a phone, a script, Node-RED or a home automation hub. In this tutorial you will build an ESP32 firmware that accepts JSON commands, runs non-blocking effects, and publishes its state as a retained message. Everything is tested with broker.connectmq.com and plain mosquitto_pub commands.

Command Format and Topics

Addressable strips have more state than a relay: power, color, brightness and effect. Sending each as a separate topic quickly becomes messy, so this project uses one JSON command topic and one JSON state topic. Every field in a command is optional, which means a client can change only the brightness without knowing the current color.

Topic Direction Retained Example
connectmq/demo/<id>/light/set App to ESP32 No {"color":[255,80,0],"brightness":120}
connectmq/demo/<id>/light/state ESP32 to apps Yes {"state":"ON","color":[255,80,0],"brightness":120,"effect":"solid"}
connectmq/demo/<id>/status ESP32 to apps Yes online / offline

Supported fields:

Field Type Meaning
state "ON" / "OFF" Turns the strip on or off without forgetting the color
color [r,g,b], 0 to 255 each Base color for solid and breathe effects
brightness 0 to 255 Global brightness, capped by a firmware limit
effect solid, rainbow, breathe, chase Animation mode
speed 1 to 100 Animation speed

This shape is close to what many home automation tools use for JSON lights, so you can adapt it to an existing setup with little effort. If you want to dig into parsing patterns, the guide to ArduinoJson with MQTT on the ESP32 goes deeper into document sizing and validation.

Hardware: Wiring and Power Done Right

Most failed LED projects fail on power, not on code. A WS2812B LED can draw a considerable current at full white, and a few meters of strip easily exceed what a USB port or the ESP32's regulator can deliver.

You need:

  • ESP32 development board.
  • WS2812B strip (this tutorial assumes 60 LEDs; set NUM_LEDS to match yours).
  • A 5 V power supply sized for your strip with headroom.
  • A 1000 µF capacitor across the strip's 5 V and GND at the input.
  • A 330 Ω resistor in series with the data line.
  • Optional but recommended: a 74AHCT125 or similar level shifter for the data signal.
Strip wire Connect to
5V Power supply +5 V
GND Power supply GND and ESP32 GND
DIN ESP32 GPIO 5 through 330 Ω (via level shifter if used)

The ESP32 outputs 3.3 V logic while WS2812B expects a high level close to 0.7 × VDD. Many strips work without a level shifter, especially with short data wires, but flickering first pixels or random colors are the classic symptom of a marginal signal. For long strips, inject power at both ends so the far LEDs do not turn orange as voltage drops along the copper.

The firmware also caps brightness with MAX_BRIGHTNESS. That is a cheap insurance policy: even if someone sends "brightness":255 with white, the strip stays inside what your supply can handle.

Libraries

Install from the Arduino Library Manager:

  • PubSubClient by Nick O'Leary.
  • Adafruit NeoPixel by Adafruit, which supports WS2812B on the ESP32.
  • ArduinoJson version 7 by Benoit Blanchon.

The ESP32 WS2812B MQTT Firmware

#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;   // protect your power supply

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);                 // room for larger JSON commands
  mqtt.setSocketTimeout(5);
}

void loop() {
  // Animated effects redraw on a timer; solid only when something changed
  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();
}

Design choices worth copying

Merge, do not replace. The callback only changes the fields present in the command. Sending {"brightness":40} dims the strip without touching the color or effect, which is what users expect from a light.

Non-blocking animation. Every effect computes one frame based on a counter and returns. There is no delay() and no for loop that runs for seconds, so mqtt.loop() keeps running and commands are applied immediately even in the middle of a rainbow.

Solid colors are not redrawn constantly. strip.show() briefly disables interrupts on some platforms while it clocks out data. Redrawing a static color hundreds of times per second wastes CPU and gives nothing back.

State is published after every command. Because the state topic is retained, a dashboard opened later sees the current color immediately.

Test with mosquitto_pub

Watch the device:

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

Send a few commands:

# Warm orange at medium brightness
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"}'

# Rainbow, faster
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-92be0f -t "connectmq/demo/leds-92be0f/light/set" -m '{"effect":"rainbow","speed":80}'

# Off, keeps the color for next time
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-92be0f -t "connectmq/demo/leds-92be0f/light/set" -m '{"state":"OFF"}'

Each command is followed by a state message such as:

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

The public MQTT test broker at broker.connectmq.com is ideal for this kind of quick experiment, but everything on it is readable by anyone. Use your own credentials before you put the strip on a shelf at home.

Drive the Strip from a Script

Command-line tests are fine, but most LED projects end up being driven by something automatic: a sunset schedule, a build status, a doorbell. The short Python script below uses paho-mqtt 2.x to fade the strip to a warm evening color and then confirms the change by reading the retained state.

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()

Because every command is a partial update and the device always answers with its full state, the script does not need to know anything about the strip's previous mode. The same idea works from Node-RED with an mqtt out node and a function node that builds the JSON object, or from a home automation hub configured for a JSON light. Keep the fade steps coarse: one command per second is plenty, and the effect engine on the ESP32 is the right place for smooth animation, not the network.

Troubleshooting Checklist

  1. First LED wrong color or flickering. Weak data signal. Add the 330 Ω resistor, shorten the data wire, and use a level shifter.
  2. Colors swapped (red shows green). Your strip uses a different byte order. Try NEO_RGB instead of NEO_GRB.
  3. ESP32 resets when the strip turns white. Supply sag. Power the strip from its own 5 V supply, share ground, lower MAX_BRIGHTNESS.
  4. Far end of the strip looks yellow or orange. Voltage drop along the strip. Inject 5 V at both ends or every couple of meters.
  5. Large JSON commands silently ignored. PubSubClient drops messages bigger than its buffer. The sketch raises it to 512 bytes; see PubSubClient buffer size limits if you send per-pixel data.
  6. Effects stutter when MQTT reconnects. A blocking connect attempt freezes the animation for up to the socket timeout. Reduce setSocketTimeout, or move networking to its own task.

Take It to Production

When the strip moves from the workbench into a living room or a shop window, connect it to your own broker. In the ConnectMQ portal, create a broker, then use the generated username, password and client ID with WiFiClientSecure on port 8883. A cloud MQTT broker means your phone, Node-RED instance and the strip can all reach the same endpoint from anywhere, without opening ports on your router.

ConnectMQ also gives you a live topic explorer, so you can watch light/state change as you test effects, and the in-browser MQTT client is a quick way to send JSON commands without a terminal. If you want the strip to show status alongside a small screen, pair it with an ESP32 OLED display showing MQTT messages, or combine it with relay switching to cut power to the strip entirely at night.

Get Started with ConnectMQ

Create a free Serverless broker at app.connectmq.com with no credit card. You get secure credentials right away and can control your WS2812B strip from anywhere within minutes.

Frequently Asked Questions

Can I control individual WS2812B pixels over MQTT?

Yes, by sending an array of colors per pixel, but keep payloads small. For long strips it is usually better to send effect names and parameters and let the ESP32 render the pixels locally.

Why does my strip flicker only when Wi-Fi is active?

The Adafruit NeoPixel library on the ESP32 uses the RMT peripheral, which is robust against Wi-Fi interrupts, so flicker during Wi-Fi activity usually points to power noise or a marginal data signal rather than timing.

How many LEDs can one ESP32 drive?

The data protocol handles hundreds of LEDs on one pin; the practical limits are power supply, frame rate and RAM. Each LED needs three bytes of buffer in RGB mode.

Should the light state be retained?

Yes. A retained state message lets any new subscriber, such as a dashboard or phone app, show the correct color immediately after it connects.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.