AccueilAteliersLED RGB et NeoPixel
ATELIER 01 ● Débutant

LED RGB et NeoPixel

Prends le contrôle d’un bandeau de 16 LED RGB WS2812. Couleurs sur mesure, animations de défilement, arc-en-ciel automatique — tout ça avec un seul fil de données.

3h

ESP32NeoPixelWS2812RGBAnimations

Ce que tu vas créer

Un bandeau NeoPixel qui propose 6 animations différentes. Le bouton BOOT de l’ESP32 fait défiler les modes :


Matériel nécessaire

ComposantQuantitéNotes
ESP321Fourni en atelier
Bandeau NeoPixel WS28121 (16 LEDs)Fourni en atelier
Condensateur 1000 µF1Protection contre les pics de courant
Résistance 300-500 Ω1Sur le fil DATA
Câble micro-USB1Fourni en atelier

Connexions

câblage
ESP32 5V   ──── NeoPixel 5V
ESP32 GND  ──── NeoPixel GND
ESP32 G18  ──── résistance 300Ω ──── NeoPixel DIN

Le code

NeoPixel MicroPython — main.py .python
atelier2-neopixel/main.py
115 lignes GitHub
# Atelier 02 — LED RGB et NeoPixel (WS2812)
# Fablab Ardèche — MicroPython
#
# Contrôle un bandeau de 16 LED RGB WS2812.
# Les boutons changent l'animation en cours.
#
# Matériel : ESP32, bandeau NeoPixel 16 LED
# Connexions : DATA → GPIO18, BP_A → GPIO0 (PULL_UP)

from machine import Pin
from neopixel import NeoPixel
import time, random

# --- Configuration ---
N       = 16       # nombre de LEDs
NP_PIN  = 18
BP_PIN  = 0        # bouton BOOT (PULL_UP)

np = NeoPixel(Pin(NP_PIN, Pin.OUT), N)
bp = Pin(BP_PIN, Pin.IN, Pin.PULL_UP)

# --- Utilitaires couleur ---
def eteindre():
    for i in range(N):
        np[i] = (0, 0, 0)
    np.write()

def roue(pos):
    """Convertit 0-255 en couleur arc-en-ciel RGB."""
    pos = pos % 256
    if pos < 85:
        return (255 - pos * 3, pos * 3, 0)
    elif pos < 170:
        pos -= 85
        return (0, 255 - pos * 3, pos * 3)
    else:
        pos -= 170
        return (pos * 3, 0, 255 - pos * 3)

# --- Animations ---
def animation_defilement(couleur=(0, 80, 255), delai=80):
    """Une LED qui se déplace de gauche à droite."""
    for i in range(N):
        eteindre()
        np[i] = couleur
        np.write()
        time.sleep_ms(delai)
        if bp.value() == 0:
            return

def animation_arc_en_ciel(tours=3):
    """Arc-en-ciel qui défile sur toutes les LEDs."""
    for _ in range(tours * 256):
        for i in range(N):
            np[i] = roue((i * 16 + _) % 256)
        np.write()
        time.sleep_ms(15)
        if bp.value() == 0:
            return

def animation_scintillement(duree_ms=3000):
    """LEDs qui s'allument et s'éteignent aléatoirement."""
    t0 = time.ticks_ms()
    while time.ticks_diff(time.ticks_ms(), t0) < duree_ms:
        i = random.randint(0, N - 1)
        np[i] = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
        np.write()
        time.sleep_ms(80)
        np[i] = (0, 0, 0)
        np.write()
        if bp.value() == 0:
            return

def animation_remplissage(couleur=(255, 50, 0), delai=120):
    """Les LEDs s'allument une par une puis s'éteignent."""
    for i in range(N):
        np[i] = couleur
        np.write()
        time.sleep_ms(delai)
        if bp.value() == 0:
            return
    time.sleep_ms(400)
    for i in range(N - 1, -1, -1):
        np[i] = (0, 0, 0)
        np.write()
        time.sleep_ms(delai)
        if bp.value() == 0:
            return

# --- Boucle principale ---
animations = [
    ("Défilement bleu",    lambda: animation_defilement((0, 80, 255))),
    ("Défilement rouge",   lambda: animation_defilement((255, 30, 0))),
    ("Arc-en-ciel",        animation_arc_en_ciel),
    ("Scintillement",      animation_scintillement),
    ("Remplissage orange", lambda: animation_remplissage((255, 50, 0))),
    ("Remplissage vert",   lambda: animation_remplissage((0, 200, 50))),
]

idx = 0
print("Appuie sur le bouton BOOT pour changer d'animation")

while True:
    nom, anim = animations[idx]
    print(f"Animation : {nom}")
    anim()

    # Attendre que le bouton soit relâché
    while bp.value() == 0:
        time.sleep_ms(20)
    time.sleep_ms(100)

    idx = (idx + 1) % len(animations)
    eteindre()
🤔 Avant de lancer Tu modifies `np[3] = (255, 0, 0)` mais tu oublies `np.write()`. Que se passe-t-il ? voir la réponse

Rien du tout. La LED 3 reste dans son état précédent.

np[3] = (255, 0, 0) ne fait que noter la couleur en mémoire, dans l’ESP32. C’est np.write() qui envoie réellement toutes les couleurs vers le bandeau, sur le fil de données.

C’est d’ailleurs pratique : on prépare les 16 LEDs tranquillement, puis un seul write() les met toutes à jour d’un coup — donc sans scintillement.

🔧 À toi de jouer
  • Change la couleur du défilement : couleur=(255, 0, 0) pour du rouge
  • Change la vitesse : delai=20 au lieu de 80
  • Baisse l’intensité : (0, 20, 60) au lieu de (0, 80, 255) — bien moins éblouissant, et ça consomme trois fois moins
  • Si ton bandeau n’a pas 16 LEDs, ajuste N = 16
  • Dans l’arc-en-ciel, remplace i * 16 par i * 4 → les couleurs se resserrent
  • Plus dur : fais un chenillard qui rebondit (aller-retour) au lieu de repartir à gauche

Comment ça marche

Couleurs RGB

Chaque LED NeoPixel est contrôlée par un triplet (rouge, vert, bleu), chaque valeur de 0 à 255 :

np[0] = (255, 0, 0)    # rouge pur
np[1] = (0, 255, 0)    # vert pur
np[2] = (0, 0, 255)    # bleu pur
np[3] = (255, 165, 0)  # orange
np[4] = (128, 0, 128)  # violet
np.write()             # envoyer vers les LEDs

Un seul fil pour 16 LEDs

C’est la particularité des WS2812 : un seul fil de données commande tout le bandeau, quelle que soit sa longueur.

Chaque LED contient un minuscule circuit qui lit les 24 premiers bits qui lui parviennent (8 pour le rouge, 8 pour le vert, 8 pour le bleu), les garde pour elle, et retransmet tout le reste à sa voisine. De proche en proche, chacune se sert au passage.

D’où deux conséquences pratiques :

La fonction arc-en-ciel

roue(pos) transforme un nombre de 0 à 255 en une couleur, en parcourant le cercle des teintes. Elle travaille par tiers :

posTransition
0 → 84rouge qui s’efface, vert qui monte
85 → 169vert qui s’efface, bleu qui monte
170 → 255bleu qui s’efface, rouge qui remonte

À chaque instant, la somme des trois composantes reste constante — la luminosité ne varie pas, seule la teinte tourne. Et comme pos = pos % 256 ramène toujours dans l’intervalle, on peut incrémenter indéfiniment : l’arc-en-ciel boucle tout seul.

Le décalage i * 16 dans l’animation donne à chaque LED une teinte légèrement différente de sa voisine (16 LEDs × 16 = 256, soit un tour complet des teintes) : c’est ce qui étale l’arc-en-ciel sur le bandeau au lieu de le faire clignoter d’un bloc.

Attention à la consommation

Une LED WS2812 à fond en blanc (255, 255, 255) tire environ 60 mA. Pour 16 LEDs, cela fait presque 1 A — bien plus que ce qu’un port USB fournit confortablement.

En atelier, on reste donc sur des couleurs modérées. Pour un bandeau plus long ou à pleine puissance, il faut une alimentation 5 V séparée, avec la masse reliée à celle de l’ESP32.


Pour aller plus loin

Contrôle par boutons

Utilise 3 boutons pour choisir la couleur rouge/vert/bleu en temps réel.

Réagir au son

Branche un micro et fais pulser les LEDs selon le volume ambiant.

Bandeau plus long

Adapte le code pour 30, 60 ou 144 LEDs — change juste N.


Six défis de plus — avec un seul bouton en plus

Pour ces défis, ajoute un bouton poussoir : une patte sur G5, l’autre sur GND (la résistance interne de l’ESP32 est activée par le programme, aucun autre composant). Le bandeau reste sur G18. Pour chaque défi, copie le fichier dans Thonny, enregistre-le sur l’ESP32 sous le nom main.py, et lance-le.

1. La LED qui avance à chaque appui

Un appui sur le bouton = un cran. Pas d’animation automatique : c’est toi qui tiens le rythme.

01_pas_a_pas.py .python
atelier2-neopixel/aller-plus-loin/01_pas_a_pas.py
40 lignes GitHub
# Atelier 02 — Aller plus loin n°1 : la LED qui avance à chaque appui
# Fablab Ardèche — MicroPython
#
# Chaque appui sur le bouton fait avancer d'un cran la LED allumée.
#
# Matériel : ESP32 + bandeau NeoPixel 16 LED + 1 bouton poussoir
# Connexions : DATA → GPIO18, bouton (BP) entre GPIO5 et GND

from machine import Pin
from neopixel import NeoPixel
import time

N = 16
np = NeoPixel(Pin(18, Pin.OUT), N)
bp = Pin(5, Pin.IN, Pin.PULL_UP)

COULEUR = (0, 40, 120)      # bleu, pas trop éblouissant


def afficher(i):
    for k in range(N):
        np[k] = (0, 0, 0)
    np[i] = COULEUR
    np.write()


position = 0
afficher(position)
print("Appuie sur le bouton pour faire avancer la LED")

while True:
    if bp.value() == 0:                 # bouton enfoncé (actif à l'état bas)
        position = (position + 1) % N
        afficher(position)
        time.sleep_ms(30)               # anti-rebond
        while bp.value() == 0:          # attendre le relâchement
            time.sleep_ms(10)
        time.sleep_ms(30)
    time.sleep_ms(5)
🔧 À toi de jouer
  • Change la couleur : COULEUR = (0, 40, 120) devient (120, 0, 0) pour du rouge
  • Fais reculer la LED au lieu de la faire avancer
  • Fais avancer la LED de deux crans à chaque appui
  • Ajoute un second sens : un appui court avance, un appui long recule

2. Un compteur binaire

Chaque LED est un bit : LED 0 = 1, LED 1 = 2, LED 2 = 4, et ainsi de suite jusqu’à 128 avec la LED 7 (le programme compte sur 8 bits, les LEDs 8 à 15 restent éteintes). C’est exactement comme ça qu’un ordinateur compte.

02_compteur_binaire.py .python
atelier2-neopixel/aller-plus-loin/02_compteur_binaire.py
50 lignes GitHub
# Atelier 02 — Aller plus loin n°2 : compteur binaire
# Fablab Ardèche — MicroPython
#
# Chaque appui court sur le bouton ajoute 1. Le bandeau affiche le nombre
# en binaire : LED allumée = bit à 1. LED 0 = poids faible (1), LED 7 = 128.
# Le compteur fait 8 bits : les LEDs 8 à 15 restent éteintes (voir BITS).
# Appui long (plus de 0,8 s) : remise à zéro.
#
# Matériel : ESP32 + bandeau NeoPixel 16 LED + 1 bouton poussoir
# Connexions : DATA → GPIO18, bouton (BP) entre GPIO5 et GND

from machine import Pin
from neopixel import NeoPixel
import time

N = 16
BITS = 8                # nombre de bits comptés (8 → de 0 à 255)
np = NeoPixel(Pin(18, Pin.OUT), N)
bp = Pin(5, Pin.IN, Pin.PULL_UP)


def afficher(valeur):
    for i in range(N):
        if (valeur >> i) & 1:           # le bit i vaut-il 1 ?
            np[i] = (0, 20, 60)
        else:
            np[i] = (0, 0, 0)
    np.write()
    print(valeur, "=", "{:0{}b}".format(valeur, BITS))


valeur = 0
afficher(valeur)
print("Appui court : +1   |   appui long : remise à zéro")

while True:
    if bp.value() == 0:
        t0 = time.ticks_ms()
        time.sleep_ms(30)               # anti-rebond
        while bp.value() == 0:
            time.sleep_ms(10)
        duree = time.ticks_diff(time.ticks_ms(), t0)
        if duree > 800:
            valeur = 0
        else:
            valeur = (valeur + 1) % (1 << BITS)
        afficher(valeur)
        time.sleep_ms(30)
    time.sleep_ms(10)
🤔 Avant de lancer Tu appuies 5 fois sur le bouton. Quelles LEDs sont allumées ? voir la réponse

Les LEDs 0 et 2.

5 = 4 + 1, soit 00000101 en binaire : la LED 0 vaut 1 et la LED 2 vaut 4. Regarde aussi le Shell de Thonny : le programme y écrit le nombre en décimal et en binaire.

Et à 255 ? Les 8 premières LEDs sont toutes allumées. Le 256ᵉ appui remet tout à zéro : avec 8 bits, on ne peut pas aller plus loin.

🔧 À toi de jouer
  • Ton bandeau a 16 LEDs : mets BITS = 16 et compte jusqu’à 65 535. Combien d’appuis pour allumer la LED 15 ? (Indice : 32 768.)
  • Change la couleur des LEDs allumées : (0, 20, 60)

3. Le jeu de réflexes

Un point bleu fait des allers-retours. Appuie sur le bouton quand il passe sur la LED verte du milieu. Chaque victoire l’accélère, une erreur remet tout à zéro.

03_jeu_reflexes.py .python
atelier2-neopixel/aller-plus-loin/03_jeu_reflexes.py
84 lignes GitHub
# Atelier 02 — Aller plus loin n°3 : jeu de réflexes
# Fablab Ardèche — MicroPython
#
# Un point bleu fait des allers-retours sur le bandeau. La LED du milieu
# est repérée en vert. Appuie sur le bouton au moment précis où le point passe
# dessus (il devient alors turquoise) !
#   - gagné : éclairs verts, et le point va plus vite
#   - raté  : éclairs rouges, retour à la vitesse de départ
#
# Matériel : ESP32 + bandeau NeoPixel 16 LED + 1 bouton poussoir
# Connexions : DATA → GPIO18, bouton (BP) entre GPIO5 et GND

from machine import Pin
from neopixel import NeoPixel
import time

N = 16
CIBLE = N // 2
DELAI_DEPART = 90       # ms entre deux pas : plus petit = plus rapide
DELAI_MIN = 25

np = NeoPixel(Pin(18, Pin.OUT), N)
bp = Pin(5, Pin.IN, Pin.PULL_UP)


def tout_eteindre():
    for i in range(N):
        np[i] = (0, 0, 0)


def afficher(pos):
    tout_eteindre()
    np[CIBLE] = (0, 25, 0)                              # repère vert
    np[pos] = (0, 60, 60) if pos == CIBLE else (0, 0, 70)
    np.write()


def eclairs(couleur):
    for _ in range(3):
        for i in range(N):
            np[i] = couleur
        np.write()
        time.sleep_ms(120)
        tout_eteindre()
        np.write()
        time.sleep_ms(120)


def attendre(delai):
    """Attend delai ms. Renvoie True si le bouton est appuyé pendant ce temps."""
    t0 = time.ticks_ms()
    while time.ticks_diff(time.ticks_ms(), t0) < delai:
        if bp.value() == 0:
            return True
        time.sleep_ms(5)
    return False


delai = DELAI_DEPART
score = 0
pos, sens = 0, 1
print("Appuie sur le bouton quand le point passe sur la LED verte")

while True:
    afficher(pos)
    if attendre(delai):
        if pos == CIBLE:
            score += 1
            delai = max(DELAI_MIN, delai - 15)
            print("Gagné ! Score :", score)
            eclairs((0, 120, 0))
        else:
            print("Raté... score final :", score)
            score = 0
            delai = DELAI_DEPART
            eclairs((150, 0, 0))
        while bp.value() == 0:          # attendre le relâchement
            time.sleep_ms(10)
        time.sleep_ms(200)
    # avancer, en rebondissant aux deux bouts
    if pos + sens < 0 or pos + sens >= N:
        sens = -sens
    pos += sens
🔧 À toi de jouer
  • Change la cible : CIBLE = 0 pour viser le bout du bandeau
  • Rends le jeu plus dur : delai - 25 au lieu de delai - 15
  • Ajoute un défi à deux : le premier qui rate perd, l’autre gagne le point

4. Un dé électronique

Le dé tire des valeurs au hasard de plus en plus lentement, puis s’arrête. Le résultat est le nombre de LEDs allumées, de 1 à 6.

04_de_electronique.py .python
atelier2-neopixel/aller-plus-loin/04_de_electronique.py
50 lignes GitHub
# Atelier 02 — Aller plus loin n°4 : dé électronique
# Fablab Ardèche — MicroPython
#
# Appuie sur le bouton pour lancer le dé. Le bandeau tire des valeurs au hasard
# de plus en plus lentement (comme un dé qui ralentit), puis s'arrête sur
# le résultat : le nombre de LEDs allumées, de 1 à 6.
#
# Matériel : ESP32 + bandeau NeoPixel 16 LED + 1 bouton poussoir
# Connexions : DATA → GPIO18, bouton (BP) entre GPIO5 et GND

from machine import Pin
from neopixel import NeoPixel
import time
import random

N = 16
np = NeoPixel(Pin(18, Pin.OUT), N)
bp = Pin(5, Pin.IN, Pin.PULL_UP)


def afficher(valeur, couleur):
    """Allume les 'valeur' premières LEDs."""
    for i in range(N):
        np[i] = couleur if i < valeur else (0, 0, 0)
    np.write()


def lancer():
    delai = 40
    valeur = 1
    while delai < 400:
        valeur = random.randint(1, 6)
        afficher(valeur, (0, 30, 60))
        time.sleep_ms(delai)
        delai = int(delai * 1.25)       # ralentit à chaque tirage
    afficher(valeur, (255, 60, 0))      # résultat final en orange
    print("Résultat :", valeur)


afficher(0, (0, 0, 0))
print("Appuie sur le bouton pour lancer le dé")

while True:
    if bp.value() == 0:
        lancer()
        while bp.value() == 0:
            time.sleep_ms(10)
        time.sleep_ms(50)
    time.sleep_ms(10)
🔧 À toi de jouer
  • Passe à un dé à 12 faces : random.randint(1, 12)
  • Change la vitesse du ralentissement : delai * 1.25 devient delai * 1.1
  • Lance le dé 60 fois : comptes-tu vraiment autant de 6 que de 1 ?

5. Une veilleuse qui respire

Tout le bandeau monte et descend en luminosité. Un appui court change la couleur, un appui long change l’intensité maximale.

05_veilleuse.py .python
atelier2-neopixel/aller-plus-loin/05_veilleuse.py
61 lignes GitHub
# Atelier 02 — Aller plus loin n°5 : veilleuse qui respire
# Fablab Ardèche — MicroPython
#
# Tout le bandeau « respire » (la luminosité monte et descend en douceur).
#   - appui COURT sur le bouton : couleur suivante
#   - appui LONG (plus de 0,6 s) : intensité maximale suivante (25 %, 50 %, 100 %)
#
# Matériel : ESP32 + bandeau NeoPixel 16 LED + 1 bouton poussoir
# Connexions : DATA → GPIO18, bouton (BP) entre GPIO5 et GND

from machine import Pin
from neopixel import NeoPixel
import time

N = 16
np = NeoPixel(Pin(18, Pin.OUT), N)
bp = Pin(5, Pin.IN, Pin.PULL_UP)

COULEURS = [
    ("orange", (255, 80, 0)),
    ("bleu",   (0, 60, 255)),
    ("vert",   (0, 220, 60)),
    ("violet", (150, 0, 200)),
    ("rose",   (255, 20, 80)),
]
INTENSITES = [1, 2, 4]          # 1 → 25 %, 2 → 50 %, 4 → 100 %

couleur_idx = 0
intensite_idx = 1
phase = 0                       # 0 à 99 : position dans la respiration
appui_debut = None

print("Appui court : couleur   |   appui long : intensité")

while True:
    # --- respiration : onde triangulaire 0 → 50 → 0 ---
    niveau = phase if phase < 50 else 100 - phase
    phase = (phase + 1) % 100
    facteur = niveau * INTENSITES[intensite_idx]        # 0 à 200
    r, v, b = COULEURS[couleur_idx][1]
    teinte = (r * facteur // 200, v * facteur // 200, b * facteur // 200)
    for i in range(N):
        np[i] = teinte
    np.write()

    # --- bouton : on mesure la durée de l'appui ---
    if bp.value() == 0:
        if appui_debut is None:
            appui_debut = time.ticks_ms()
    elif appui_debut is not None:
        duree = time.ticks_diff(time.ticks_ms(), appui_debut)
        appui_debut = None
        if duree > 600:
            intensite_idx = (intensite_idx + 1) % len(INTENSITES)
            print("Intensité :", INTENSITES[intensite_idx] * 25, "%")
        elif duree > 30:
            couleur_idx = (couleur_idx + 1) % len(COULEURS)
            print("Couleur :", COULEURS[couleur_idx][0])

    time.sleep_ms(20)
🔧 À toi de jouer
  • Ajoute une couleur à la liste COULEURS
  • Ralentis la respiration : remplace time.sleep_ms(20) par time.sleep_ms(40)
  • Le programme distingue un appui court d’un appui long : trouve où, et change la limite de 600 ms

6. Le chenillard qui rebondit

C’est le défi proposé plus haut, avec une traînée qui s’éteint derrière le point rouge. Un appui sur le bouton change la vitesse.

06_chenillard_rebond.py .python
atelier2-neopixel/aller-plus-loin/06_chenillard_rebond.py
46 lignes GitHub
# Atelier 02 — Aller plus loin n°6 : chenillard qui rebondit, avec traînée
# Fablab Ardèche — MicroPython
#
# Le défi de la page « Toi de jouer » : un point rouge fait des allers-retours
# et laisse derrière lui une traînée qui s'éteint doucement (effet K2000).
# Le bouton change la vitesse (lente / moyenne / rapide).
#
# Matériel : ESP32 + bandeau NeoPixel 16 LED + 1 bouton poussoir
# Connexions : DATA → GPIO18, bouton (BP) entre GPIO5 et GND

from machine import Pin
from neopixel import NeoPixel
import time

N = 16
np = NeoPixel(Pin(18, Pin.OUT), N)
bp = Pin(5, Pin.IN, Pin.PULL_UP)

VITESSES = [("lente", 80), ("moyenne", 40), ("rapide", 15)]
vitesse_idx = 1

pos, sens = 0, 1
print("Appuie sur le bouton pour changer de vitesse")

while True:
    # 1) toutes les LEDs s'atténuent (on garde les 2/3 de leur valeur)
    for i in range(N):
        r, v, b = np[i]
        np[i] = (r * 2 // 3, v * 2 // 3, b * 2 // 3)
    # 2) la tête est allumée à fond
    np[pos] = (255, 0, 0)
    np.write()

    # 3) rebond aux deux extrémités
    if pos + sens < 0 or pos + sens >= N:
        sens = -sens
    pos += sens

    time.sleep_ms(VITESSES[vitesse_idx][1])

    if bp.value() == 0:
        vitesse_idx = (vitesse_idx + 1) % len(VITESSES)
        print("Vitesse :", VITESSES[vitesse_idx][0])
        while bp.value() == 0:
            time.sleep_ms(10)
🔧 À toi de jouer
  • Allonge la traînée : remplace * 2 // 3 par * 4 // 5
  • Change la couleur de la tête : (255, 0, 0) devient (0, 100, 255)
  • Fais varier la couleur de la tête à chaque rebond