NeoPixel progressif
Afficher une valeur numérique sur un bandeau WS2812 sans saut visible — interpolation entre deux LEDs adjacentes avec correction gamma. Classe réutilisable : jauge, VU-mètre, niveau d'ascenseur, barre de progression.
Le problème des positions discrètes
Un bandeau NeoPixel de 8 LEDs ne peut afficher que 8 positions. Si une valeur passe de 374 à 376, rien ne bouge — puis d'un coup une LED bascule. Sur un affichage de niveau (ascenseur, jauge, VU-mètre), cet effet de saut est très visible.
❌ Sans interpolation
La LED s'allume ou s'éteint d'un coup. Sur 8 LEDs pour 1000 valeurs, chaque LED couvre 125 unités — saut très visible à l'œil.
✓ Avec interpolation
Deux LEDs adjacentes sont allumées proportionnellement. La progression paraît continue même avec seulement 8 LEDs physiques.
L'algorithme en deux étapes
La position d'une valeur x tombe en général entre deux LEDs.
On calcule quelle fraction de chaque LED allumer :
| Variable | Calcul | Signification |
|---|---|---|
| entier | x * n // max | Index de la LED "juste avant" la position |
| frac2 | (x*n/max - entier) * 255 | Luminosité de la LED suivante (0–255) |
| frac1 | 255 - frac2 | Luminosité de la LED courante (complément) |
La correction gamma
Même avec interpolation linéaire, la transition peut paraître brusque. L'œil perçoit la lumière de façon non-linéaire : il est très sensible aux faibles luminosités et peu sensible aux fortes. La correction gamma compense ça :
c2 = int((frac2 / 255) ** coef * 255) # LED suivante — courbe en puissance
c1 = int((frac1 / 255) ** coef * 255) # LED courante
Avec coef = 1.8, les valeurs faibles sont "tassées" et les fortes amplifiées — la progression paraît régulière.
Augmenter coef adoucit encore la transition. Valeur 1.0 = linéaire (pas de correction).
⚠️ Algorithme original
La combinaison interpolation fractionnelle + correction gamma appliquée à la fraction (et non à la luminosité finale) est absente des bibliothèques NeoPixel existantes — ni en MicroPython ni en Arduino. C'est un algorithme développé spécifiquement pour ce module.
Fichiers commentés
Chaque fichier est précédé de son contexte. Le code est affiché directement — bouton copier + lien GitHub disponibles.
🌈 La classe NeoProgressif
Le module réutilisable — fonctionne indépendamment de tout projet GRAFCET.
1 neoprog.py — Interpolation + correction gamma
La classe NeoProgressif encapsule un bandeau NeoPixel et expose une seule méthode utile : afficher(x). Elle gère en interne l'interpolation et la correction gamma.
pin— numéro de broche GPIO du bandeaun— nombre de LEDs (défaut : 8)coef— exposant gamma (défaut : 1.8)couleur— tuple RGB (défaut : bleu)
Usage minimal :
from neoprog import NeoProgressif
neo = NeoProgressif(pin=26, n=8, couleur=(0, 255, 0))
neo.afficher(500) # position milieu
neo.eteindre() # tout éteindre neoprog.py — NeoPixel progressif avec correction gamma .python avance/neoprog/neoprog.py 102 lignes
GitHub
# =============================================================================
# neoprog.py — Affichage progressif sur bandeau NeoPixel
# =============================================================================
# Classe NeoProgressif : affiche une valeur numérique sur un bandeau WS2812
# avec interpolation douce entre les LEDs (pas de saut visible).
#
# PRINCIPE :
# Une valeur x dans [0, max] est mappée sur n LEDs.
# La position tombe en général ENTRE deux LEDs.
# → les deux LEDs adjacentes sont allumées proportionnellement,
# avec correction gamma pour que l'œil perçoive une transition régulière.
#
# USAGE :
# from neoprog import NeoProgressif
#
# neo = NeoProgressif(pin=26, n=8) # bandeau sur Pin 26, 8 LEDs
# neo.afficher(500) # position milieu
# neo.eteindre() # tout éteindre
# =============================================================================
from neopixel import NeoPixel # pilotage des LEDs WS2812
from machine import Pin # accès aux broches ESP32
class NeoProgressif:
"""
Affichage progressif d'une valeur sur un bandeau NeoPixel.
Interpole la luminosité entre deux LEDs adjacentes pour un rendu fluide.
"""
def __init__(self, pin, n=8, coef=1.8, couleur=(0, 0, 255)):
"""
Initialise le bandeau NeoPixel.
:param pin: numéro de broche GPIO (ex: 26 sur la carte ENIM)
:param n: nombre de LEDs du bandeau (défaut : 8)
:param coef: exposant de correction gamma (défaut : 1.8)
— compense la perception non-linéaire de l'œil
— augmenter pour une transition plus "en douceur"
:param couleur: tuple RGB de la couleur d'affichage (défaut : bleu)
"""
self.np = NeoPixel(Pin(pin), n) # objet NeoPixel sur la broche
self.n = n # nombre de LEDs
self.coef = coef # coefficient gamma
self.couleur = couleur # couleur RGB (0-255 par canal)
self.eteindre() # éteindre toutes les LEDs au démarrage
def afficher(self, x, max=1000):
"""
Affiche la valeur x sur le bandeau avec interpolation douce.
:param x: valeur à afficher, dans [0, max]
:param max: valeur maximale de l'échelle (défaut : 1000)
Exemple :
neo.afficher(0) → bandeau éteint (position 0)
neo.afficher(500) → position centrale, 2 LEDs à 50%
neo.afficher(1000) → bandeau plein
"""
# --- Calcul de la position ---
# entier : index de la LED "juste avant" la position (0 à n-1)
entier = x * self.n // max
# frac2 : fraction de la LED suivante (0 = pas allumée, 255 = pleine)
# représente à quel % on est entre entier et entier+1
frac2 = int((x * self.n / max - entier) * 255)
# frac1 : complément — luminosité de la LED courante
frac1 = 255 - frac2
# --- Correction gamma ---
# Sans correction, l'œil perçoit les transitions de façon non-linéaire.
# La courbe en puissance (** coef) rend la progression perçue régulière.
c2 = int((frac2 / 255) ** self.coef * 255) # luminosité LED suivante
c1 = int((frac1 / 255) ** self.coef * 255) # luminosité LED courante
# --- Calcul des couleurs avec luminosité appliquée ---
r, g, b = self.couleur
couleur2 = (r * c2 // 255, g * c2 // 255, b * c2 // 255) # LED suivante
couleur1 = (r * c1 // 255, g * c1 // 255, b * c1 // 255) # LED courante
# --- Éteindre tout le bandeau ---
for i in range(self.n):
self.np[i] = (0, 0, 0)
# --- Allumer les deux LEDs de la transition ---
if entier != self.n: # évite le débordement en fin de bandeau
self.np[entier] = couleur2
if entier > 0: # évite l'index -1 en début de bandeau
self.np[entier - 1] = couleur1
self.np.write() # envoyer les données au bandeau
def eteindre(self):
"""Éteint toutes les LEDs du bandeau."""
for i in range(self.n):
self.np[i] = (0, 0, 0)
self.np.write()
🏗️ L'ascenseur avec NeoPixel progressif
Le même ascenseur GRAFCET, enrichi d'un affichage visuel fluide du niveau.
1 ascenseur_enim_v2.py — Grafcet + NeoProgressif
Cette version combine les deux modules : grafcet.py pour la logique séquentielle et neoprog.py pour l'affichage. La cabine simulée descend de 0 à -100 — convertie en valeur 0–1000 pour NeoProgressif :
neo.afficher(int(-niveau * 10))
# niveau=0 → afficher(0) → bandeau éteint (cabine en haut)
# niveau=-100 → afficher(1000) → bandeau plein (cabine en bas) Fichiers nécessaires sur l'ESP32 : grafcet.py, neoprog.py, essential.py (sans OLED).
ascenseur_enim_v2.py — Ascenseur ENIM avec NeoProgressif .python avance/neoprog/ascenseur_enim_v2.py 133 lignes
GitHub
# =============================================================================
# ascenseur_enim_v2.py — Ascenseur ENIM avec affichage NeoPixel progressif
# =============================================================================
# Version 2 : utilise NeoProgressif pour un affichage fluide du niveau.
#
# Fichiers nécessaires sur l'ESP32 :
# grafcet.py (moteur GRAFCET)
# neoprog.py (affichage NeoPixel progressif)
# essential.py (déclarations carte ENIM — sans OLED)
#
# GRAFCET (inchangé) :
# Étape 0 — Repos │ T0 : bpA ET tempo[0] > 200 ms
# Étape 1 — Descente │ T1 : bpC OU niveau ≤ -99
# Étape 2 — Montée │ T2 : bpB OU niveau ≥ -1
# =============================================================================
from machine import Pin
from grafcet import Grafcet
from neoprog import NeoProgressif # ← nouveau : affichage fluide
from essential import (
synchro_ms,
bpA, bpB, bpC,
led_bleue, led_verte, led_jaune,
)
# --- NeoProgressif sur Pin 26, 8 LEDs, couleur verte ---
# On ne récupère plus np depuis essential : NeoProgressif crée son propre NeoPixel
neo = NeoProgressif(pin=26, n=8, couleur=(0, 255, 0))
# --- Broches moteur sur les connecteurs libres ---
sortie_descente = Pin(12, Pin.OUT)
sortie_montee = Pin(13, Pin.OUT)
sortie_descente.value(0)
sortie_montee.value(0)
# =============================================================================
# GRAFCET
# =============================================================================
g = Grafcet(nb_etapes=3, etape_initiale=0)
T = [
(0, (0,), (1,)), # Repos → Descente
(1, (1,), (2,)), # Descente → Montée
(2, (2,), (0,)), # Montée → Repos
]
# =============================================================================
# VARIABLES
# =============================================================================
Descendre = False
Monter = False
Start = False
Haut = False
Bas = False
niveau = 0 # position simulée : 0 = haut, -100 = bas
vitesse = 1 # déplacement par cycle
transitions = [False] * len(T)
# =============================================================================
# SIMULATION DU NIVEAU — NeoProgressif
# =============================================================================
def ascenseur(inc):
"""
Déplace la cabine simulée et met à jour le NeoPixel progressivement.
:param inc: -vitesse = descente, +vitesse = montée
"""
global niveau
niveau = niveau + inc
if niveau < -100: niveau = -100
if niveau > 0: niveau = 0
# Conversion niveau [-100, 0] → valeur [1000, 0] pour NeoProgressif
# niveau=0 → x=0 (bandeau éteint, cabine en haut)
# niveau=-100 → x=1000 (bandeau plein, cabine en bas)
neo.afficher(int(-niveau * 10))
# =============================================================================
# CYCLE GRAFCET
# =============================================================================
def gerer_actions():
global Descendre, Monter
if g.etapes[0]: Descendre = False ; Monter = False
if g.etapes[1]: Descendre = True ; Monter = False
if g.etapes[2]: Descendre = False ; Monter = True
def affecter_sorties():
led_bleue.value(g.etapes[0])
led_verte.value(Descendre)
led_jaune.value(Monter)
sortie_descente.value(Descendre)
sortie_montee.value(Monter)
if Descendre: ascenseur(-vitesse)
if Monter: ascenseur(+vitesse)
def lire_entrees():
global Start, Haut, Bas
Start = bpA.value()
Bas = bpC.value() or (niveau <= -99)
Haut = bpB.value() or (niveau >= -1)
def calculer_transitions():
transitions[0] = g.etapes[0] and Start and (g.tempo[0] > 200)
transitions[1] = g.etapes[1] and Bas
transitions[2] = g.etapes[2] and Haut
# =============================================================================
# BOUCLE PRINCIPALE
# =============================================================================
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
calculer_transitions()
synchro_ms(20)
Réutiliser NeoProgressif ailleurs
Copie neoprog.py dans n'importe quel projet. La classe est indépendante de GRAFCET — utilisable pour une jauge de température, un VU-mètre audio, une barre de progression, etc.