ESP32 et Arduino

WS2812B MQTT : piloter un ruban LED adressable avec des commandes JSON

Pilotez un ruban LED adressable WS2812B depuis un ESP32 avec des commandes MQTT en JSON pour la couleur, la luminosité et les effets, et publiez un état retenu pour que chaque tableau de bord reste synchronisé.

Ruban LED WS2812B coloré piloté par un ESP32 dans un projet d'éclairage WS2812B MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Un contrôleur WS2812B MQTT vous permet de changer la couleur, la luminosité et l'animation d'un ruban LED adressable depuis un téléphone, un script, Node-RED ou la box domotique de votre maison connectée. Dans ce tutoriel, vous allez construire un firmware ESP32 qui accepte des commandes JSON, exécute des effets non bloquants et publie son état sous forme de retained message. Tout est testé avec broker.connectmq.com et de simples commandes mosquitto_pub.

Format des commandes et topics

Un ruban adressable a plus d'état qu'un relais : alimentation, couleur, luminosité et effet. Envoyer chacun sur un topic séparé devient vite désordonné ; ce projet utilise donc un topic de commande JSON et un topic d'état JSON. Chaque champ d'une commande est facultatif : un client peut ainsi modifier uniquement la luminosité sans connaître la couleur actuelle.

Topic Sens Retained Exemple
connectmq/demo/<id>/light/set Application vers ESP32 Non {"color":[255,80,0],"brightness":120}
connectmq/demo/<id>/light/state ESP32 vers applications Oui {"state":"ON","color":[255,80,0],"brightness":120,"effect":"solid"}
connectmq/demo/<id>/status ESP32 vers applications Oui online / offline

Champs pris en charge :

Champ Type Signification
state "ON" / "OFF" Allume ou éteint le ruban sans oublier la couleur
color [r,g,b], de 0 à 255 chacun Couleur de base pour les effets solid et breathe
brightness 0 à 255 Luminosité globale, plafonnée par une limite du firmware
effect solid, rainbow, breathe, chase Mode d'animation
speed 1 à 100 Vitesse d'animation

Cette structure est proche de celle qu'utilisent de nombreux outils domotiques pour les lumières JSON : vous pouvez donc l'adapter à une installation existante sans grand effort. Jeedom et Domoticz savent aussi communiquer en MQTT. Pour approfondir les techniques d'analyse, le guide ArduinoJson avec MQTT sur ESP32 détaille le dimensionnement des documents et la validation.

Matériel : un câblage et une alimentation dans les règles

La plupart des projets LED ratés échouent à cause de l'alimentation, pas du code. Une LED WS2812B peut tirer un courant considérable en blanc à pleine puissance, et quelques mètres de ruban dépassent facilement ce que peuvent fournir un port USB ou le régulateur de l'ESP32.

Il vous faut :

  • Une carte de développement ESP32.
  • Un ruban WS2812B (ce tutoriel suppose 60 LED ; réglez NUM_LEDS selon votre ruban).
  • Une alimentation 5 V dimensionnée pour votre ruban, avec de la marge.
  • Un condensateur de 1000 µF entre le 5 V et le GND du ruban, à l'entrée.
  • Une résistance de 330 Ω en série sur la ligne de données.
  • Facultatif mais recommandé : un 74AHCT125 ou un convertisseur de niveau similaire pour le signal de données.
Fil du ruban À relier à
5V +5 V de l'alimentation
GND GND de l'alimentation et GND de l'ESP32
DIN GPIO 5 de l'ESP32 via 330 Ω (via le convertisseur de niveau le cas échéant)

L'ESP32 sort une logique 3,3 V alors que le WS2812B attend un niveau haut proche de 0,7 × VDD. Beaucoup de rubans fonctionnent sans convertisseur de niveau, surtout avec des fils de données courts, mais des premiers pixels qui scintillent ou des couleurs aléatoires sont le symptôme classique d'un signal limite. Pour les longs rubans, injectez l'alimentation aux deux extrémités afin que les LED éloignées ne virent pas à l'orange à cause de la chute de tension le long du cuivre.

Le firmware plafonne aussi la luminosité avec MAX_BRIGHTNESS. C'est une assurance peu coûteuse : même si quelqu'un envoie "brightness":255 en blanc, le ruban reste dans les capacités de votre alimentation.

Bibliothèques

À installer depuis le gestionnaire de bibliothèques Arduino :

  • PubSubClient de Nick O'Leary.
  • Adafruit NeoPixel d'Adafruit, qui prend en charge le WS2812B sur ESP32.
  • ArduinoJson version 7 de Benoit Blanchon.

Le 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;   // protège votre alimentation

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);                 // de la place pour des commandes JSON plus grandes
  mqtt.setSocketTimeout(5);
}

void loop() {
  // Les effets animés sont redessinés sur un timer ; la couleur fixe seulement en cas de changement
  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();
}

Des choix de conception à reprendre

Fusionner, ne pas remplacer. Le callback ne modifie que les champs présents dans la commande. Envoyer {"brightness":40} atténue le ruban sans toucher à la couleur ni à l'effet, ce que les utilisateurs attendent d'une lumière.

Animation non bloquante. Chaque effet calcule une image à partir d'un compteur puis rend la main. Il n'y a ni delay() ni boucle for qui tourne pendant des secondes : mqtt.loop() continue donc de s'exécuter et les commandes sont appliquées immédiatement, même au milieu d'un arc-en-ciel.

Les couleurs fixes ne sont pas redessinées en permanence. Sur certaines plateformes, strip.show() désactive brièvement les interruptions pendant l'envoi des données. Redessiner une couleur statique des centaines de fois par seconde gaspille du CPU sans rien apporter.

L'état est publié après chaque commande. Comme le topic d'état est retenu, un tableau de bord ouvert plus tard voit immédiatement la couleur actuelle.

Tester avec mosquitto_pub

Observez l'appareil :

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

Envoyez quelques commandes :

# Orange chaud à luminosité moyenne
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"}'

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

# Éteint, en gardant la couleur pour la prochaine fois
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-92be0f -t "connectmq/demo/leds-92be0f/light/set" -m '{"state":"OFF"}'

Chaque commande est suivie d'un message d'état comme :

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

Le serveur MQTT public gratuit broker.connectmq.com est idéal pour ce genre d'expérience rapide et pour les prototypes, mais tout ce qui s'y trouve est lisible par n'importe qui. Utilisez vos propres identifiants avant d'installer le ruban sur une étagère chez vous.

Piloter le ruban depuis un script

Les tests en ligne de commande sont pratiques, mais la plupart des projets LED finissent pilotés par quelque chose d'automatique : un horaire de coucher de soleil, un statut de build, une sonnette. Le court script Python ci-dessous utilise paho-mqtt 2.x pour faire passer progressivement le ruban à une couleur chaude de soirée, puis confirme le changement en lisant l'état retenu.

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

Comme chaque commande est une mise à jour partielle et que l'appareil répond toujours avec son état complet, le script n'a besoin de rien savoir du mode précédent du ruban. La même idée fonctionne depuis Node-RED avec un nœud mqtt out et un nœud function qui construit l'objet JSON, ou depuis une box domotique configurée pour une lumière JSON. Gardez des paliers de transition grossiers : une commande par seconde suffit largement, et c'est le moteur d'effets de l'ESP32, et non le réseau, qui doit assurer les animations fluides.

Liste de vérification pour le dépannage

  1. La première LED a une mauvaise couleur ou scintille. Signal de données faible. Ajoutez la résistance de 330 Ω, raccourcissez le fil de données et utilisez un convertisseur de niveau.
  2. Couleurs inversées (le rouge s'affiche en vert). Votre ruban utilise un autre ordre d'octets. Essayez NEO_RGB au lieu de NEO_GRB.
  3. L'ESP32 redémarre quand le ruban passe au blanc. Chute de l'alimentation. Alimentez le ruban avec sa propre alimentation 5 V, partagez la masse, réduisez MAX_BRIGHTNESS.
  4. L'extrémité du ruban paraît jaune ou orange. Chute de tension le long du ruban. Injectez le 5 V aux deux extrémités ou tous les deux mètres environ.
  5. Les grosses commandes JSON sont ignorées sans bruit. PubSubClient abandonne les messages plus gros que son buffer. Le sketch le porte à 512 octets ; consultez les limites de taille du buffer PubSubClient si vous envoyez des données par pixel.
  6. Les effets saccadent lors des reconnexions MQTT. Une tentative de connexion bloquante fige l'animation jusqu'à l'expiration du délai du socket. Réduisez setSocketTimeout, ou déplacez la partie réseau dans sa propre tâche.

Passer en production

Quand le ruban quitte l'établi pour un salon ou une vitrine, connectez-le à votre propre broker. Dans le portail ConnectMQ, créez un broker, puis utilisez le nom d'utilisateur, le mot de passe et le client ID générés avec WiFiClientSecure sur le port 8883. Un broker MQTT managé, c'est-à-dire un broker MQTT cloud exploité pour vous, permet à votre téléphone, à votre instance Node-RED et au ruban d'atteindre le même point d'accès depuis n'importe où, sans ouvrir de ports sur votre routeur.

ConnectMQ vous offre aussi un explorateur de topics en direct pour voir light/state évoluer pendant que vous testez les effets, et le client MQTT dans le navigateur est un moyen rapide d'envoyer des commandes JSON sans terminal. Si vous voulez que le ruban affiche un statut à côté d'un petit écran, associez-le à un écran OLED ESP32 affichant des messages MQTT, ou combinez-le à une commutation par relais pour couper complètement l'alimentation du ruban la nuit.

Démarrer avec ConnectMQ

Créez un broker Serverless gratuit sur app.connectmq.com sans carte bancaire. Vous obtenez immédiatement des identifiants sécurisés et pouvez piloter votre ruban WS2812B depuis n'importe où en quelques minutes.

Questions fréquentes

Puis-je piloter des pixels WS2812B individuels via MQTT ?

Oui, en envoyant un tableau de couleurs par pixel, mais gardez des payloads réduits. Pour les longs rubans, il vaut généralement mieux envoyer des noms d'effets et des paramètres, et laisser l'ESP32 calculer les pixels localement.

Pourquoi mon ruban scintille-t-il uniquement quand le Wi-Fi est actif ?

La bibliothèque Adafruit NeoPixel utilise sur ESP32 le périphérique RMT, résistant aux interruptions Wi-Fi : un scintillement pendant l'activité Wi-Fi indique donc généralement du bruit d'alimentation ou un signal de données limite plutôt qu'un problème de timing.

Combien de LED un ESP32 peut-il piloter ?

Le protocole de données gère des centaines de LED sur une seule broche ; les limites pratiques sont l'alimentation, la fréquence d'images et la RAM. Chaque LED nécessite trois octets de buffer en mode RGB.

L'état de la lumière doit-il être retenu ?

Oui. Un message d'état retenu permet à tout nouvel abonné, comme un tableau de bord ou une application mobile, d'afficher la bonne couleur dès sa connexion.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.