This commit is contained in:
bach
2026-10-08 15:25:14 +02:00
parent 32e617f67b
commit da4fac3af7
64 changed files with 3921 additions and 0 deletions
+502
View File
@@ -0,0 +1,502 @@
"""
=============================================================================
MATRICE LED WS2812B 8 x 32 (256 pixels) — le « moteur » pour Raspberry Pi
Workshop « Vitrine pixels »
=============================================================================
Ce fichier est la SALLE DES MACHINES. Vous n'avez normalement pas besoin
de le modifier, sauf les RÉGLAGES de la partie 1. Les autres programmes
du dossier s'en servent :
- lecteur.py joue les animations dessinées dans Krita (GIF, PNG…) ;
- test_matrice.py vérifie le branchement et l'orientation ;
- mes_animations.py animations écrites en code (comme sur l'Arduino).
CE QUI CHANGE PAR RAPPORT À L'ARDUINO
-------------------------------------
Le principe électrique est EXACTEMENT le même que dans le dossier
matrice_8x32 : les 256 LED forment une chaîne, et on leur envoie une
longue liste de couleurs par UN SEUL FIL de données.
Ce qui change, c'est le « cerveau » :
- l'Arduino est un petit microcontrôleur : il ne fait qu'une chose,
le programme qu'on lui a téléversé ;
- le Raspberry Pi est un vrai petit ORDINATEUR (avec Linux, des
fichiers, le réseau…). Il peut donc LIRE DES IMAGES : on n'a plus
besoin de traduire chaque dessin en code.
Le langage aussi change : Python au lieu du C++ de l'Arduino.
Python se lit presque comme de l'anglais, et il n'y a pas besoin de
« téléverser » : on lance le programme directement.
DEUX ÉCRANS POSSIBLES
---------------------
Ce moteur sait afficher l'image :
1. sur la VRAIE matrice, si on est sur un Raspberry Pi avec la
bibliothèque rpi_ws281x installée ;
2. dans un SIMULATEUR : une fenêtre sur votre ordinateur, avec 256
ronds de couleur. Pratique pour préparer ses animations chez soi,
sans matériel.
Le choix se fait tout seul (ou avec l'option --simulateur).
Il existe aussi un 3e « écran », l'ENREGISTREUR : avec l'option
--enregistrer fichier.gif, les images sont enregistrées dans un GIF animé.
ORGANISATION DU FICHIER
-----------------------
1. Réglages : taille, broche, luminosité, limite de courant, orientation.
2. Les couleurs (avec leur nom en français).
3. La fonction XY() : de la position (x, y) au numéro de LED.
4. La correction gamma et la limite de courant.
5. Les écrans : la vraie matrice, le simulateur et l'enregistreur.
6. Les outils de dessin : pixel(), effacer(), montrer(), attendre()…
"""
import colorsys # conversions de couleurs (teinte → rouge, vert, bleu)
import sys # pour lire les options de la ligne de commande
import time # pour mesurer et attendre le temps qui passe
# Pillow est la bibliothèque Python qui sait lire et manipuler les images.
# Si elle manque : sudo apt install python3-pil (sur le Pi)
# pip install pillow (sur un ordinateur)
from PIL import Image
# =============================================================================
# 1. RÉGLAGES
# =============================================================================
# Taille de la matrice : 32 colonnes (largeur) × 8 lignes (hauteur).
LARGEUR = 32
HAUTEUR = 8
NB_LEDS = LARGEUR * HAUTEUR # = 256
# Broche du Raspberry Pi reliée (à travers l'adaptateur 74AHCT125) au fil
# VERT (DIN) de la matrice. Ce numéro est le numéro « GPIO », pas le numéro
# de la patte : GPIO 18 = patte physique n° 12 du connecteur du Pi.
# Ne changez pas sans raison : seules quelques broches savent produire
# ce signal très rapide (GPIO 18 est la plus simple).
BROCHE_GPIO = 18
# SIGNAL INVERSÉ : à mettre à True UNIQUEMENT si le signal passe par un
# transistor (montage « sans 74AHCT125 », solution B du README). Un transistor
# inverse le signal (un 1 devient un 0) : la bibliothèque l'inverse alors
# une seconde fois avant de l'envoyer, et tout redevient juste.
INVERSER_SIGNAL = False
# LUMINOSITÉ générale, de 0 (éteint) à 255 (maximum).
# Ces LED sont TRÈS lumineuses : 40 suffit en intérieur.
# En vitrine, en plein jour, on pourra monter vers 100 ou plus,
# à condition d'avoir une alimentation assez puissante.
LUMINOSITE = 40
# SÉCURITÉ : courant maximum autorisé, en milliampères (1000 mA = 1 A).
# Une LED en blanc à fond consomme environ 60 mA (20 mA par couleur).
# 256 LED × 60 mA = plus de 15 A ! L'alimentation en fournit beaucoup moins.
# Avant chaque image, le moteur CALCULE ce que les LED vont consommer et,
# si ça dépasse cette limite, il baisse la luminosité de cette image.
# → Réglez cette valeur UN PEU EN DESSOUS de la limite de l'alimentation
# (ex. : alimentation d'établi limitée à 3 A → 2500 mA ici).
COURANT_MAX_MA = 2500
# ORIENTATION de la matrice (voir test_matrice.py).
# Le pixel ROUGE du test doit être EN HAUT À GAUCHE, le VERT EN HAUT À DROITE.
# - rouge et vert inversés (gauche/droite) → INVERSER_X = True
# - rouge et vert en bas au lieu d'en haut → INVERSER_Y = True
INVERSER_X = False
INVERSER_Y = False
# CORRECTION GAMMA (voir partie 4). 2.2 = comme un écran d'ordinateur.
# 1.0 = pas de correction (les couleurs sombres paraîtront trop claires).
GAMMA = 2.2
# =============================================================================
# 2. LES COULEURS
# =============================================================================
#
# En Python, une couleur est un « tuple » de trois nombres entre
# parenthèses : (rouge, vert, bleu), chacun de 0 à 255.
# (255, 0, 0) = rouge vif
# (255, 128, 0) = orange
# (20, 0, 40) = violet sombre
#
# On donne un nom aux plus courantes. Les noms en MAJUSCULES sont, par
# convention, des valeurs qui ne changent pas (des « constantes »).
NOIR = (0, 0, 0)
BLANC = (255, 255, 255)
ROUGE = (255, 0, 0)
ORANGE = (255, 100, 0)
JAUNE = (255, 200, 0)
VERT = (0, 255, 0)
CYAN = (0, 255, 255)
BLEU = (0, 0, 255)
VIOLET = (130, 0, 255)
MAGENTA = (255, 0, 255)
ROSE = (255, 20, 100)
def teinte(t, saturation=255, valeur=255):
"""Fabrique une couleur à partir d'une TEINTE, comme CHSV dans FastLED.
t (la teinte) va de 0 à 255 et fait le tour de l'arc-en-ciel :
0 rouge, 32 orange, 64 jaune, 96 vert, 128 cyan, 160 bleu,
192 violet, 224 rose… puis retour au rouge (256 = 0).
saturation : 0 = gris/blanc, 255 = couleur pure.
valeur : 0 = noir, 255 = pleine lumière.
"""
# colorsys travaille avec des nombres entre 0 et 1 : on divise par 255,
# puis on remultiplie le résultat par 255.
# « % 256 » = reste de la division par 256 : 300 devient 44, la teinte boucle.
r, v, b = colorsys.hsv_to_rgb((t % 256) / 256, saturation / 255, valeur / 255)
return (round(r * 255), round(v * 255), round(b * 255))
# =============================================================================
# 3. DE LA POSITION (x, y) AU NUMÉRO DE LED
# =============================================================================
#
# Pour dessiner, on pense en coordonnées :
# x = colonne, de 0 (à gauche) à 31 (à droite)
# y = ligne, de 0 (en haut) à 7 (en bas)
# Le point (0, 0) est en haut à gauche, comme dans Krita.
#
# Mais les LED sont numérotées dans l'ordre de la chaîne, qui fait des
# zigzags (« serpentin ») : elle descend la 1re colonne, remonte la 2e…
#
# x=0 x=1 x=2 x=3
# y=0 0 15 16 31 …
# y=1 1 14 17 30
# … … … … …
# y=7 7 → 8 23 → 24
#
# C'est la même fonction que dans matrice_8x32.ino, traduite en Python.
def XY(x, y):
"""Renvoie le numéro (0 à 255) de la LED placée en colonne x, ligne y."""
if INVERSER_X:
x = (LARGEUR - 1) - x
if INVERSER_Y:
y = (HAUTEUR - 1) - y
if x % 2 == 0:
# Colonne paire : la chaîne DESCEND.
return x * HAUTEUR + y
else:
# Colonne impaire : la chaîne REMONTE.
return x * HAUTEUR + (HAUTEUR - 1 - y)
# =============================================================================
# 4. CORRECTION GAMMA ET LIMITE DE COURANT
# =============================================================================
#
# POURQUOI UNE « CORRECTION GAMMA » ?
# Notre œil n'est pas une règle graduée : il est beaucoup plus sensible aux
# différences dans les sombres que dans les clairs. Les écrans d'ordinateur
# en tiennent compte : dans Krita, un gris à 128 (la moitié de 255) ne
# produit en réalité qu'environ 22 % de la lumière du blanc, et c'est ce
# que notre œil perçoit comme « gris moyen ».
#
# Les LED, elles, sont « honnêtes » : 128 donne VRAIMENT 50 % de lumière,
# ce que l'œil voit comme presque blanc. Sans correction, les dégradés
# dessinés dans Krita paraîtraient délavés sur la matrice.
#
# On prépare donc une TABLE de 256 cases : pour chaque valeur de 0 à 255
# venue de l'image, la valeur à envoyer à la LED pour que ça « ressemble »
# à l'écran. Calculée une seule fois, au démarrage.
#
# Conséquence à connaître : les couleurs très sombres de l'image deviennent
# 0 (éteint) sur la matrice. Voir le GUIDE_KRITA.md.
TABLE_GAMMA = [round(255 * (i / 255) ** GAMMA) for i in range(256)]
# (Cette ligne se lit : « pour chaque i de 0 à 255, calculer 255 × (i/255)^gamma ».
# Les crochets fabriquent une liste : c'est une « liste en compréhension ».)
# Consommation approximative d'une LED, pour la limite de courant.
MA_PAR_COULEUR = 20 # une couleur (R, G ou B) à fond ≈ 20 mA
MA_AU_REPOS = 1 # la petite puce de chaque LED consomme ≈ 1 mA même éteinte
def pixels_de(image):
"""Liste des couleurs (r, v, b) de tous les pixels d'une image RVB,
ligne par ligne : (0,0), (1,0), (2,0)… puis la ligne suivante.
(tobytes() donne les octets r, v, b, r, v, b… : on les regroupe par 3.)"""
octets = image.tobytes()
return [tuple(octets[i:i + 3]) for i in range(0, len(octets), 3)]
def preparer_valeurs(image):
"""Transforme une image Pillow en liste de 256 couleurs prêtes à envoyer.
Étapes : correction gamma → luminosité → limite de courant.
Renvoie une liste de tuples (r, v, b), dans l'ordre des lignes de l'image
(pixel (0,0), (1,0), (2,0)… puis la ligne suivante).
"""
valeurs = []
for (r, v, b) in pixels_de(image): # tous les pixels, un par un
valeurs.append((
TABLE_GAMMA[r] * LUMINOSITE // 255, # « // » = division entière
TABLE_GAMMA[v] * LUMINOSITE // 255,
TABLE_GAMMA[b] * LUMINOSITE // 255,
))
# Estimation du courant : on additionne toutes les composantes.
total = sum(r + v + b for (r, v, b) in valeurs)
repos = NB_LEDS * MA_AU_REPOS
courant = repos + total * MA_PAR_COULEUR / 255
if courant > COURANT_MAX_MA:
# Trop gourmand : on baisse toute l'image dans la bonne proportion.
facteur = (COURANT_MAX_MA - repos) / (courant - repos)
valeurs = [(int(r * facteur), int(v * facteur), int(b * facteur))
for (r, v, b) in valeurs]
return valeurs
def courant_estime_ma(image):
"""Courant (en mA) que consommerait cette image, limite comprise."""
valeurs = preparer_valeurs(image)
total = sum(r + v + b for (r, v, b) in valeurs)
return NB_LEDS * MA_AU_REPOS + total * MA_PAR_COULEUR / 255
# =============================================================================
# 5. LES ÉCRANS
# =============================================================================
#
# Une « classe » est un modèle d'objet : elle regroupe des données et les
# fonctions (on dit « méthodes ») qui s'en servent. Nos deux écrans ont les
# mêmes méthodes : afficher(image), attendre(secondes) et eteindre().
# Le reste du programme peut donc utiliser l'un ou l'autre sans changer
# une ligne : c'est tout l'intérêt.
class MatriceReelle:
"""La vraie matrice, branchée sur le Raspberry Pi."""
def __init__(self):
# On importe la bibliothèque ICI, et pas en haut du fichier :
# comme ça, le simulateur fonctionne sur un ordinateur où elle
# n'est pas installée.
from rpi_ws281x import PixelStrip, Color, ws
self.Color = Color
self.bande = PixelStrip(
NB_LEDS, # nombre de LED
BROCHE_GPIO, # broche des données (GPIO 18)
freq_hz=800_000, # 800 000 bits par seconde, comme FastLED
dma=10, # canal interne utilisé pour envoyer les bits
invert=INVERSER_SIGNAL, # True seulement avec le montage à transistor
brightness=255, # on gère la luminosité nous-mêmes (partie 4)
channel=0,
strip_type=ws.WS2811_STRIP_GRB, # ordre des couleurs : Vert, Rouge, Bleu
)
self.bande.begin() # démarre l'envoi (il faut être administrateur : sudo)
def afficher(self, image):
valeurs = preparer_valeurs(image)
for y in range(HAUTEUR):
for x in range(LARGEUR):
r, v, b = valeurs[y * LARGEUR + x]
self.bande.setPixelColor(XY(x, y), self.Color(r, v, b))
self.bande.show() # envoie les 256 couleurs dans le fil (≈ 8 ms)
def attendre(self, secondes):
if secondes > 0:
time.sleep(secondes)
def eteindre(self):
for i in range(NB_LEDS):
self.bande.setPixelColor(i, 0)
self.bande.show()
class Simulateur:
"""Une fenêtre sur l'ordinateur qui imite la matrice (256 ronds)."""
TAILLE = 26 # taille d'un pixel à l'écran, en points
MARGE = 4 # espace entre deux pixels
def __init__(self, titre="Simulateur — matrice 8 × 32"):
# tkinter est la bibliothèque de fenêtres livrée avec Python.
import tkinter
self.fenetre = tkinter.Tk()
self.fenetre.title(titre)
self.fenetre.configure(background="black")
pas = self.TAILLE + self.MARGE
self.toile = tkinter.Canvas(self.fenetre, width=LARGEUR * pas + self.MARGE,
height=HAUTEUR * pas + self.MARGE,
background="#111", highlightthickness=0)
self.toile.pack(padx=10, pady=10)
# Un rond par pixel. On garde leurs numéros pour changer leur couleur.
self.ronds = {}
for y in range(HAUTEUR):
for x in range(LARGEUR):
x0 = self.MARGE + x * pas
y0 = self.MARGE + y * pas
self.ronds[(x, y)] = self.toile.create_oval(
x0, y0, x0 + self.TAILLE, y0 + self.TAILLE, fill="#000", outline="#222")
# Quand on ferme la fenêtre, on arrête proprement le programme.
self.ouverte = True
self.fenetre.protocol("WM_DELETE_WINDOW", self._fermer)
self.fenetre.update()
def _fermer(self):
self.ouverte = False
def _rafraichir(self):
if not self.ouverte:
self.fenetre.destroy()
raise SystemExit("Fenêtre du simulateur fermée.")
self.fenetre.update()
def afficher(self, image):
# Le simulateur montre les couleurs de l'image telles quelles :
# l'écran de l'ordinateur fait déjà sa propre « correction gamma ».
for y in range(HAUTEUR):
for x in range(LARGEUR):
r, v, b = image.getpixel((x, y))
self.toile.itemconfig(self.ronds[(x, y)], fill=f"#{r:02x}{v:02x}{b:02x}")
self._rafraichir()
def attendre(self, secondes):
# On ne peut pas simplement dormir : la fenêtre se figerait.
# On attend par petits bouts en la laissant se redessiner.
fin = time.monotonic() + secondes
while True:
self._rafraichir()
reste = fin - time.monotonic()
if reste <= 0:
break
time.sleep(min(reste, 0.01))
def eteindre(self):
if self.ouverte:
self.afficher(Image.new("RGB", (LARGEUR, HAUTEUR)))
class Enregistreur:
"""Un « écran » qui n'affiche rien : il ENREGISTRE chaque image montrée
dans un GIF animé. Une animation écrite en code devient ainsi un fichier,
que le lecteur jouera comme un dessin de Krita.
Option : --enregistrer animations/10_mon_animation.gif"""
def __init__(self, fichier):
self.fichier = fichier
self.images = []
self.durees = [] # en secondes, une par image
def afficher(self, image):
self.images.append(image.copy()) # copy() : une photo de la toile à cet instant
self.durees.append(0)
def attendre(self, secondes):
# Le temps d'attente devient la durée de l'image affichée juste avant.
if self.durees and secondes > 0:
self.durees[-1] += secondes
def eteindre(self):
if not self.images:
return
# Le format GIF ne sait pas afficher une image moins de 20 ms.
durees_ms = [max(20, round(d * 1000)) for d in self.durees]
self.images[0].save(self.fichier, save_all=True, append_images=self.images[1:],
duration=durees_ms, loop=0)
print(f"Enregistré : {self.fichier} ({len(self.images)} images, "
f"{sum(durees_ms) / 1000:.1f} s)")
self.images = []
def ouvrir_ecran(simulateur=None):
"""Choisit l'écran : vraie matrice si possible, sinon simulateur.
simulateur = True → toujours le simulateur ;
simulateur = None → décide tout seul (option --simulateur sur la ligne
de commande, ou bibliothèque rpi_ws281x absente).
Avec l'option « --enregistrer fichier.gif », on enregistre au lieu d'afficher.
"""
if "--enregistrer" in sys.argv:
position = sys.argv.index("--enregistrer")
if position + 1 >= len(sys.argv):
raise SystemExit("Indiquez un nom de fichier après --enregistrer")
print("Écran : enregistrement dans", sys.argv[position + 1])
return Enregistreur(sys.argv[position + 1])
if simulateur is None:
simulateur = "--simulateur" in sys.argv
if not simulateur:
try:
ecran = MatriceReelle()
print("Écran : vraie matrice (GPIO %d)." % BROCHE_GPIO)
return ecran
except ImportError:
print("Bibliothèque rpi_ws281x absente : on ouvre le simulateur.")
except RuntimeError as erreur:
# Erreur typique : programme lancé sans « sudo ».
print("Impossible de parler à la matrice :", erreur)
print("Sur le Raspberry Pi, lancez le programme avec sudo.")
raise SystemExit(1)
print("Écran : simulateur.")
return Simulateur()
# =============================================================================
# 6. LES OUTILS DE DESSIN
# =============================================================================
#
# Les mêmes que sur l'Arduino : on dessine sur une TOILE invisible
# (une image Pillow de 32 × 8 pixels), puis on la montre d'un coup.
toile = Image.new("RGB", (LARGEUR, HAUTEUR)) # l'image en préparation
_ecran = None # l'écran, ouvert par demarrer()
def demarrer(simulateur=None):
"""À appeler une fois au début du programme : ouvre l'écran."""
global _ecran # « global » : on modifie la variable du fichier, pas une copie
_ecran = ouvrir_ecran(simulateur)
effacer()
montrer()
def pixel(x, y, couleur):
"""Colore le pixel (x, y). En dehors de la matrice : on ne fait rien."""
if 0 <= x < LARGEUR and 0 <= y < HAUTEUR:
toile.putpixel((int(x), int(y)), tuple(couleur))
def lire_pixel(x, y):
"""Renvoie la couleur actuelle du pixel (x, y) sur la toile."""
if 0 <= x < LARGEUR and 0 <= y < HAUTEUR:
return toile.getpixel((int(x), int(y)))
return NOIR
def effacer(couleur=NOIR):
"""Remplit toute la toile d'une couleur (noir par défaut = tout éteindre)."""
toile.paste(tuple(couleur), (0, 0, LARGEUR, HAUTEUR))
def assombrir(pourcentage):
"""Assombrit toute la toile, comme fadeToBlackBy() de FastLED.
assombrir(25) = chaque pixel perd 25 % de sa lumière."""
garde = 1 - pourcentage / 100
toile.putdata([(int(r * garde), int(v * garde), int(b * garde))
for (r, v, b) in pixels_de(toile)])
def montrer(image=None):
"""Affiche la toile (ou une autre image de 32 × 8) sur l'écran."""
_ecran.afficher(image if image is not None else toile)
def attendre(millisecondes):
"""Attend, en millisecondes, comme delay() sur l'Arduino."""
_ecran.attendre(millisecondes / 1000)
def eteindre():
"""Éteint toute la matrice. À appeler à la fin du programme."""
if _ecran is not None:
_ecran.eteindre()