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
+2
View File
@@ -0,0 +1,2 @@
apercus/
__pycache__/
+265
View File
@@ -0,0 +1,265 @@
# Coder ses animations en Python
Krita sert à **dessiner** des animations image par image. Le code sert à les
**calculer** : du hasard, des mouvements continus, des formules, des
réactions à un capteur… des choses très longues, voire impossibles, à
dessiner à la main.
Ce guide s'adresse à ceux qui ont joué avec le code Arduino
([`matrice_8x32.ino`](../matrice_8x32/matrice_8x32.ino)) et veulent continuer
sur le Raspberry Pi. Le fichier de travail est
[`mes_animations.py`](mes_animations.py) : **les mêmes animations que sur
l'Arduino, traduites en Python**. Ouvrez les deux côte à côte !
---
## Sommaire
1. [Lancer un programme](#1-lancer-un-programme)
2. [Python pour qui connaît l'Arduino](#2-python-pour-qui-connaît-larduino)
3. [Les outils de dessin](#3-les-outils-de-dessin)
4. [Exercices](#4-exercices)
5. [Mélanger code et images](#5-mélanger-code-et-images)
6. [Transformer son code en GIF](#6-transformer-son-code-en-gif)
7. [Comment est construit le moteur](#7-comment-est-construit-le-moteur)
---
## 1. Lancer un programme
Pas de téléversement en Python : on **lance** le fichier directement.
| Où | Commande |
|---|---|
| Sur son ordinateur, avec le simulateur | `python3 mes_animations.py --simulateur` (Windows : `py` au lieu de `python3`) |
| Sur le Pi, sur la vraie matrice | `sudo ~/vitrine-env/bin/python mes_animations.py` |
**Ctrl + C** (ou fermer la fenêtre du simulateur) pour arrêter.
> Sur le Pi, si le lecteur tourne déjà en mode vitrine, arrêtez-le d'abord :
> `sudo systemctl stop vitrine-pixels`. Un seul programme à la fois peut
> parler à la matrice.
Pour **éditer** le fichier : sur l'ordinateur, n'importe quel éditeur de
texte (le plus confortable : **Thonny**, <https://thonny.org>, conçu pour
apprendre Python, ou **VS Code**). Sur le Pi : `nano mes_animations.py`.
---
## 2. Python pour qui connaît l'Arduino
| | Arduino (C++) | Python |
|---|---|---|
| Fin d'instruction | `;` obligatoire | rien |
| Bloc (dans une boucle, une fonction…) | entre `{ }` | **décalé de 4 espaces** après un `:` |
| Commentaire | `// texte` | `# texte` |
| Variable | `int x = 3;` | `x = 3` |
| Fonction | `void anim() { … }` | `def anim():` + bloc décalé |
| Boucle « pour » | `for (int x = 0; x < 32; x++) { … }` | `for x in range(32):` |
| Compte à rebours | `for (int y = 7; y > 0; y--)` | `for y in range(7, 0, -1):` |
| Condition | `if (x > 3) { … } else { … }` | `if x > 3:` … `else:` … |
| « et », « ou », « non » | `&&`, `\|\|`, `!` | `and`, `or`, `not` |
| Vrai / faux | `true` / `false` | `True` / `False` (majuscule !) |
| Tableau | `int t[3] = {1, 2, 3};` | `t = [1, 2, 3]` (une « liste ») |
| Couleur | `CRGB(255, 0, 0)` | `(255, 0, 0)` (un « tuple ») |
| Couleur par teinte | `CHSV(160, 255, 255)` | `teinte(160)` |
| Hasard | `random(32)` | `random.randrange(32)` |
| Attendre 100 ms | `delay(100);` | `attendre(100)` |
| Afficher | `FastLED.show();` | `montrer()` |
| Boucle principale | `void loop() { … }` | `while True:` + bloc décalé |
**Le piège n° 1 de Python : l'indentation.** Les espaces en début de ligne
ne sont pas décoratifs, ils **sont** la structure du programme :
```python
for x in range(32):
pixel(x, 0, ROUGE) # dans la boucle : exécuté 32 fois
montrer() # dans la boucle aussi
attendre(1000) # APRÈS la boucle : exécuté une fois
```
Utilisez toujours **4 espaces** (Thonny et VS Code le font tout seuls avec
la touche Tab). Ne mélangez jamais espaces et tabulations.
**Le message d'erreur est votre ami.** Python indique le fichier, la ligne
et la nature de l'erreur, tout en bas :
```
File "mes_animations.py", line 72
attendre(20
^
SyntaxError: '(' was never closed
```
---
## 3. Les outils de dessin
Définis dans [`matrice.py`](matrice.py), disponibles dans `mes_animations.py`
grâce à la ligne `from matrice import *`.
| Outil | Effet |
|---|---|
| `pixel(x, y, couleur)` | colore le pixel de la colonne `x` (0 à 31) et de la ligne `y` (0 à 7). En dehors : ignoré |
| `lire_pixel(x, y)` | renvoie la couleur actuelle d'un pixel |
| `effacer()` | éteint toute la toile |
| `effacer(couleur)` | remplit toute la toile d'une couleur |
| `assombrir(25)` | assombrit toute la toile de 25 % (pour les traînées) |
| `montrer()` | **affiche** la toile. Rien n'apparaît tant qu'on ne l'appelle pas ! |
| `montrer(image)` | affiche une image Pillow de 32 × 8 (voir partie 5) |
| `attendre(100)` | attend 100 millisecondes |
| `teinte(t)` | couleur de l'arc-en-ciel : 0 rouge, 32 orange, 64 jaune, 96 vert, 128 cyan, 160 bleu, 192 violet, 224 rose |
| `teinte(t, saturation, valeur)` | idem, avec saturation (0 = blanc) et valeur (0 = noir), de 0 à 255 |
**Couleurs nommées** : `NOIR`, `BLANC`, `ROUGE`, `ORANGE`, `JAUNE`, `VERT`,
`CYAN`, `BLEU`, `VIOLET`, `MAGENTA`, `ROSE`.
**Dimensions** : `LARGEUR` (32), `HAUTEUR` (8).
Le point `(0, 0)` est **en haut à gauche**, comme dans Krita.
---
## 4. Exercices
Pour chaque exercice, écrivez une nouvelle fonction `def anim_…():` dans
`mes_animations.py` et ajoutez son nom dans `playlist()`. Testez dans le
simulateur.
**1. Le point qui traverse.** Un pixel blanc va de gauche à droite sur la
ligne 3, puis revient. *(Indice : deux boucles `for`, la seconde avec
`range(31, -1, -1)`.)*
**2. Le balayage.** Une colonne entière (8 pixels) balaie la matrice de
gauche à droite. *(Indice : une boucle sur `x`, et dedans une boucle sur `y`.)*
**3. Le dégradé fixe.** Remplissez la matrice d'un arc-en-ciel immobile, de
`teinte(0)` à gauche à `teinte(255)` à droite. Puis faites-le défiler.
*(Comparez avec `anim_arc_en_ciel`.)*
**4. La balle qui rebondit.** Un pixel avance d'un pas en `x` et en `y` à
chaque image, et change de sens quand il touche un bord.
```python
def anim_balle():
x, y = 0, 0 # position
dx, dy = 1, 1 # vitesse (pas par image)
for image in range(300):
effacer()
pixel(x, y, ORANGE)
montrer()
attendre(40)
x = x + dx
y = y + dy
if x <= 0 or x >= LARGEUR - 1:
dx = -dx # rebond sur un bord gauche/droite
if y <= 0 or y >= HAUTEUR - 1:
dy = -dy # rebond en haut/en bas
```
Variante : laissez une traînée en remplaçant `effacer()` par `assombrir(30)`.
**5. Le ciel étoilé.** Des pixels blancs apparaissent au hasard, très
faiblement, puis s'allument et s'éteignent doucement. *(Indice : `assombrir`
et `random.randrange`.)*
**6. Le feu.** Le bas de la matrice est orange vif, des « flammes » montent
au hasard en s'assombrissant et en tirant vers le rouge. *(Indice : partez
de `anim_pluie`, et faites monter au lieu de descendre.)*
**7. Le dessin en lettres.** Modifiez `DESSIN` dans `mes_animations.py` pour
faire défiler votre propre motif. Créez-en un deuxième, et faites-les défiler
l'un derrière l'autre.
---
## 5. Mélanger code et images
Le code peut **lire les images de l'équipe Krita** et les transformer. On
utilise Pillow, la bibliothèque d'images :
```python
from PIL import Image
def anim_logo_qui_defile():
# Remplacez par le chemin d'une image de 32 × 8 exportée depuis Krita.
logo = Image.open("animations/logo.png").convert("RGB")
for i in range(100):
effacer()
# On « colle » le logo deux fois, côte à côte, décalé de i pixels :
# ce qui sort à droite rentre à gauche (défilement en boucle).
toile.paste(logo, (i % LARGEUR - LARGEUR, 0))
toile.paste(logo, (i % LARGEUR, 0))
montrer()
attendre(60)
```
Ici `toile` est l'image de 32 × 8 sur laquelle dessinent `pixel()` et
`effacer()`. Tout ce que sait faire Pillow (coller, retourner, changer les
couleurs, écrire du texte…) fonctionne dessus. Documentation :
<https://pillow.readthedocs.io>.
---
## 6. Transformer son code en GIF
Une animation codée peut devenir un **fichier GIF**, que le lecteur jouera
en vitrine avec les dessins de l'équipe Krita :
```bash
python3 mes_animations.py --enregistrer animations/10_mes_animations.gif
```
Au lieu d'afficher, le moteur **enregistre** chaque image montrée par
`montrer()`, avec la durée de l'`attendre()` qui suit. La playlist est jouée
**une seule fois**, puis le GIF est écrit. Vérifiez-le ensuite :
```bash
python3 outils/verifier_animation.py animations/10_mes_animations.gif
```
> **Attention au hasard** : un GIF enregistre **une** version de
> l'animation. Si votre animation utilise `random`, elle sera identique à
> chaque passage dans le GIF, alors qu'en direct elle change à chaque fois.
---
## 7. Comment est construit le moteur
Pour les curieux, [`matrice.py`](matrice.py) est découpé comme le programme
Arduino, et commenté partie par partie :
1. **Réglages** : taille, broche GPIO, luminosité, limite de courant,
orientation, gamma.
2. **Couleurs** : les noms, et `teinte()` (l'équivalent de `CHSV`).
3. **`XY()`** : la même fonction que sur l'Arduino, qui transforme une
position (x, y) en numéro de LED dans le serpentin.
4. **Gamma et courant** : `preparer_valeurs()` corrige les couleurs avec une
table calculée au démarrage, applique la luminosité, estime le courant
et assombrit l'image si elle consomme trop (comme
`setMaxPowerInVoltsAndMilliamps` de FastLED).
5. **Les écrans** : trois « classes » qui savent toutes `afficher()`,
`attendre()` et `eteindre()` :
- `MatriceReelle` : envoie les couleurs aux LED avec `rpi_ws281x` ;
- `Simulateur` : dessine 256 ronds dans une fenêtre `tkinter` ;
- `Enregistreur` : accumule les images dans un GIF.
Le reste du programme ne sait pas lequel il utilise : c'est le principe
d'une **interface commune**. Ajouter un 4e écran (une page web, une
deuxième matrice…) ne demanderait pas de toucher aux animations.
6. **Outils de dessin** : `pixel()`, `effacer()`, `montrer()`…, qui
travaillent sur la `toile`.
Et [`lecteur.py`](lecteur.py) n'est qu'une boucle : lister le dossier,
charger chaque fichier en liste de couples (image, durée), et les montrer
l'un après l'autre.
### Idées pour aller plus loin
- **Un bouton** branché sur une broche GPIO pour passer à l'animation
suivante (bibliothèque `gpiozero`, déjà installée sur Raspberry Pi OS).
- **Un capteur de présence** (PIR) : la matrice s'anime quand quelqu'un
passe devant la vitrine.
- **L'heure ou la météo** en texte défilant (la police 5 × 7 est dans
[`outils/generer_exemples.py`](outils/generer_exemples.py)).
- **Une page web** sur le Pi pour choisir l'animation depuis son téléphone
(bibliothèque `flask`).
+367
View File
@@ -0,0 +1,367 @@
# Guide Krita : dessiner pour la matrice LED
Pour **l'équipe design**. Pas besoin de matériel ni de programmation : on
dessine dans Krita, on exporte, on vérifie sur son ordinateur, et on dépose
le fichier dans le dossier des animations.
![De Krita à la matrice](schemas/chaine_krita_matrice.svg)
> **En une phrase** : un document de **32 × 8 pixels**, un pixel = une LED,
> des couleurs **franches**, un fond **noir**, et un export en **séquence de
> PNG** (ou en GIF).
---
## Sommaire
1. [Comprendre la « toile »](#1-comprendre-la--toile-)
2. [Créer le document](#2-créer-le-document)
3. [Dessiner en pixel art](#3-dessiner-en-pixel-art)
4. [Animer](#4-animer)
5. [Ce qui change entre l'écran et les LED](#5-ce-qui-change-entre-lécran-et-les-led)
6. [Exporter](#6-exporter)
7. [Prévisualiser sur son ordinateur](#7-prévisualiser-sur-son-ordinateur)
8. [Déposer l'animation](#8-déposer-lanimation)
9. [Conseils de composition pour une vitrine](#9-conseils-de-composition-pour-une-vitrine)
10. [Liste de contrôle avant de rendre une animation](#10-liste-de-contrôle-avant-de-rendre-une-animation)
---
## 1. Comprendre la « toile »
La matrice est un écran de **32 colonnes × 8 lignes** = 256 pixels. Chaque
pixel est une LED, et les LED sont espacées de **1 cm** : la matrice mesure
32 × 8 cm.
```
x → 0 31
y ↓ ┌────────────────────────────────┐
0 │■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │
│ │ 8 lignes
7 │■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │
└────────────────────────────────┘
32 colonnes
```
Quelques ordres de grandeur pour se repérer :
- un caractère lisible fait **5 pixels de large × 7 de haut** : on écrit
**5 lettres** à la fois, pas plus. Un texte plus long doit **défiler** ;
- un visage simple tient dans **8 × 8** ;
- une ligne de 1 pixel d'épaisseur se voit très bien : c'est une rangée de LED.
> **La matrice finale fera 39 × 7.** Si vous préparez déjà des animations
> pour elle, créez des documents de **39 × 7** : sur la matrice d'essai, le
> lecteur les centrera (et coupera ce qui dépasse sur les côtés).
---
## 2. Créer le document
1. **Fichier → Nouveau** (*File → New*) → **Document personnalisé**.
2. Réglages :
| Réglage | Valeur |
|---|---|
| Largeur | **32** pixels |
| Hauteur | **8** pixels |
| Résolution | peu importe (72 ppp) |
| Modèle de couleur | **RVB/Alpha**, **8 bits** entier, profil **sRGB** (celui par défaut) |
| Contenu / arrière-plan | **noir** (ou transparent : le transparent devient noir, donc éteint) |
3. **Zoomez** énormément (touche `+`, ou la molette avec `Ctrl`) : à 3200 %,
chaque pixel fait environ 3 cm à l'écran.
4. **Grille de pixels** : Krita dessine une fine grille quand on zoome
beaucoup. Si elle n'apparaît pas : **Configuration → Configurer Krita →
Affichage → Grille de pixels**, et baissez le seuil d'affichage.
> ⚠️ **Ne dessinez pas en grand pour réduire ensuite.** Une image de 1920 ×
> 480 réduite à 32 × 8 devient une bouillie floue. Dessinez **directement à
> la taille réelle** : c'est le principe du pixel art.
### Importer la palette « Vitrine pixels »
Le fichier [`krita/palette_vitrine_pixels.gpl`](krita/palette_vitrine_pixels.gpl)
contient 37 couleurs choisies pour bien rendre sur les LED (couleurs pures,
moyennes, sombres et pastel, plus noir, gris et blanc).
1. **Configuration → Gérer les ressources…** → **Importer des ressources**.
2. Type de ressource : **Palettes** ; choisissez le fichier `.gpl`.
3. Dans le panneau **Palette** (*Configuration → Panneaux → Palette*),
sélectionnez « Vitrine pixels (LED) ».
---
## 3. Dessiner en pixel art
La règle d'or : **pas de flou, pas de pixels « intermédiaires »**. Chaque
pixel a une couleur franche, choisie exprès.
| Outil | Réglage |
|---|---|
| **Brosse** | un préréglage **pixel art** (tapez « pixel » dans la recherche du panneau *Préréglages de brosses*), taille **1 px** |
| **Gomme** | même brosse, touche `E` pour basculer en mode gomme |
| **Remplissage** (pot de peinture) | décocher **Anticrénelage** (*Anti-aliasing*) dans les options de l'outil |
| **Sélections** | décocher **Anticrénelage** dans les options de l'outil |
| **Transformation** (déplacer, tourner, agrandir) | Filtre : **Plus proche voisin** (*Nearest neighbour*) ; sinon les bords deviennent flous |
| **Image → Mettre à l'échelle** | Filtre : **Boîte / Plus proche voisin** (*Box / Nearest neighbour*) |
**Comment savoir s'il y a du flou ?** L'outil
[`verifier_animation.py`](#7-prévisualiser-sur-son-ordinateur) compte les
couleurs différentes : au-delà de 64, il y a sans doute de l'anticrénelage
ou un dégradé non voulu.
---
## 4. Animer
1. **Fenêtre → Espace de travail → Animation** : Krita affiche le
**montage** (*timeline*) et les **pelures d'oignon** (*onion skin*, qui
montrent en transparence l'image d'avant et d'après).
2. **Images par seconde** : réglez-les dans le panneau *Animation* (ou
*Montage*) **dès le début**. Conseils :
- **12 i/s** : le rythme classique du dessin animé, idéal en pixel art ;
- **24 i/s** : pour des mouvements fluides (deux fois plus d'images à dessiner) ;
- un **texte qui défile** d'1 pixel par image : 12 à 20 i/s.
3. Pour créer une image : clic droit sur une case du montage → **Créer une
image vide** ou **Dupliquer l'image**.
4. **Durée** : réglez la plage de lecture (début / fin) pour qu'elle
corresponde exactement à votre animation. Une animation **en boucle**
doit avoir une dernière image qui « enchaîne » bien avec la première.
> **Animation courte ?** Pas de souci : le lecteur rejoue chaque animation
> jusqu'à ce qu'elle ait duré au moins **10 secondes**. Un cœur qui bat en
> 1 seconde battra 10 fois.
---
## 5. Ce qui change entre l'écran et les LED
L'écran de votre ordinateur et la matrice ne produisent pas la lumière de la
même façon. Le programme compense au mieux (c'est la **correction gamma**),
mais certaines choses ne passent pas :
### Les couleurs très sombres disparaissent
En intérieur, la matrice tourne à environ **15 % de sa puissance** (sinon elle
éblouit). Chaque LED n'a que 256 niveaux de lumière ; à 15 %, il n'en reste
qu'une quarantaine, et les plus sombres valent **zéro**.
| Couleur dans Krita (valeur, V de TSV) | Sur la matrice |
|---|---|
| 100 % à 60 % | ✅ bien visible |
| 60 % à 35 % | ✅ visible, sombre |
| 35 % à 20 % | ⚠️ très faible, les nuances se mélangent |
| moins de 20 % | ❌ **éteint** |
**Conséquence** : les dégradés subtils, les ombres légères, les fonds
« presque noirs » ne se voient pas. Travaillez par **aplats** francs, comme
une affiche sérigraphiée.
### Les couleurs saturées rendent mieux
- Les LED excellent dans les **couleurs pures** : rouge, vert, bleu, et leurs
mélanges vifs (jaune, cyan, magenta, orange, violet).
- Les **marrons**, les **beiges**, les **gris colorés** sont décevants : ils
ressemblent à de l'orange terne ou à du blanc sale.
- Le **blanc** tire souvent un peu vers le bleu. Un « blanc chaud »
(255, 200, 140) est plus agréable.
### Le noir n'est pas du noir, c'est de l'éteint
Sur la matrice, le noir = LED éteinte = on voit la surface du panneau (ou ce
qu'il y a derrière la vitrine). **Le fond noir est la norme.** Un grand fond
coloré est possible, mais il éblouit et consomme beaucoup.
### La consommation électrique
Plus il y a de pixels allumés et clairs, plus la matrice consomme. Une image
**entièrement blanche** dépasse ce que l'alimentation peut fournir : le
programme baisse alors automatiquement la luminosité de cette image. Ce n'est
pas dangereux, mais l'image paraîtra plus terne que les autres.
`verifier_animation.py` vous le signale.
---
## 6. Exporter
**Fichier → Rendu de l'animation…** (*File → Render Animation*).
### Méthode conseillée : séquence de PNG
C'est la plus fiable : pas de perte de couleurs, pas de logiciel
supplémentaire.
1. Choisissez **Séquence d'images** (*Image Sequence*), format **PNG**.
2. **Dossier** : créez un dossier **au nom de l'animation**, par exemple
`05_feu_de_camp`.
3. **Nom de base** : `feu` → Krita crée `feu_0000.png`, `feu_0001.png`…
4. Taille : laissez **32 × 8**.
5. **Rendu**.
6. Dans ce dossier, créez un fichier texte **`images_par_seconde.txt`** qui
contient seulement le nombre d'images par seconde de votre animation,
par exemple `12`. (Sans ce fichier, le lecteur joue à 12 i/s.)
Résultat :
```
05_feu_de_camp/
├── feu_0000.png
├── feu_0001.png
├── …
└── images_par_seconde.txt ← contient « 12 »
```
### Autre méthode : GIF animé
Un seul fichier, pratique à partager, et la vitesse est enregistrée dedans.
1. Choisissez **Vidéo** et le format **GIF**.
2. Krita a besoin du logiciel gratuit **FFmpeg** pour ça. S'il le demande :
téléchargez-le (<https://ffmpeg.org/download.html>) et indiquez son
emplacement dans la fenêtre de rendu.
3. Taille **32 × 8**. Nommez le fichier, par exemple `05_feu_de_camp.gif`.
> **Limites du GIF** : 256 couleurs maximum (largement assez ici), et une
> durée d'image arrondie au centième de seconde. Le **PNG animé (APNG)** et le
> **WebP animé** fonctionnent aussi, si vous préférez.
### Ce qui ne marche pas
- Les fichiers **`.kra`** (le format de travail de Krita) : exportez toujours.
- Les **vidéos MP4, WebM, MKV** : ce sont des formats compressés qui abîment
les pixels. Utilisez PNG ou GIF.
### Une image fixe
Un logo, une affiche : **Fichier → Exporter…** en **PNG**. Le lecteur
l'affiche 10 secondes.
---
## 7. Prévisualiser sur son ordinateur
Deux outils, qui fonctionnent sans la matrice. Il faut **Python** et la
bibliothèque **Pillow** (une seule fois) :
| Système | Installer Python | Puis installer Pillow |
|---|---|---|
| **Windows** | <https://www.python.org/downloads/> (cochez **« Add python.exe to PATH »**) | `py -m pip install pillow` |
| **macOS** | <https://www.python.org/downloads/> | `python3 -m pip install pillow` |
| **Linux** | déjà installé | `sudo apt install python3-pil python3-tk` |
Ensuite, ouvrez un terminal **dans le dossier `8x32 PI`** (sous Windows,
remplacez `python3` par `py` dans les commandes ci-dessous).
### Le vérificateur
```bash
python3 outils/verifier_animation.py chemin/vers/05_feu_de_camp
```
Il affiche un rapport :
```
05_feu_de_camp
Type : séquence de 24 fichiers (12 images/s)
Taille : 32 × 8 ✓
Images : 24, durée 2.00 s (≈ 12.0 images/s)
Couleurs : 9 différentes ✓
Trop sombre : aucune ✓
Courant : moyen 0.61 A, maximum 0.84 A ✓
Aperçu : apercus/05_feu_de_camp_apercu.png et apercus/05_feu_de_camp_apercu.gif
```
Et il fabrique dans le dossier `apercus` une **planche** (toutes les images
l'une sous l'autre) et un **GIF agrandi « façon LED »**, à partager avec
l'équipe.
| Message | Signification |
|---|---|
| `⚠ attendu 32 × 8` | mauvaise taille de document : l'image sera centrée et coupée |
| `⚠ beaucoup ! anticrénelage ?` | plus de 64 couleurs : du flou s'est glissé dans le dessin |
| `Trop sombre : N couleur(s) seront ÉTEINTES` | des couleurs trop foncées (partie 5) |
| `⚠ limité par le logiciel` | image trop claire et trop remplie : elle sera assombrie |
### Le simulateur
```bash
python3 lecteur.py --simulateur chemin/vers/05_feu_de_camp
```
Une fenêtre s'ouvre et joue l'animation **en boucle, à la bonne vitesse**,
avec 256 ronds qui imitent les LED. Sans chemin, il joue tout le dossier
`animations`. Fermez la fenêtre pour arrêter.
> Le simulateur montre les couleurs **telles que dans Krita**. Il ne montre
> pas la disparition des couleurs très sombres : pour ça, le vérificateur ou,
> mieux, **la vraie matrice**. Testez sur la matrice le plus tôt possible.
---
## 8. Déposer l'animation
Le lecteur joue **tout ce qui se trouve dans le dossier des animations**, par
**ordre alphabétique**. Pour choisir l'ordre, commencez les noms par un
numéro : `01_`, `02_`, `03_`…
**Noms de fichiers** : sans accent, sans espace, sans caractère spécial.
✅ `07_pluie_de_coeurs.gif` ❌ `Pluie de cœurs (v2 finale!!).gif`
Selon l'organisation de l'équipe :
- **Carte SD** : on éteint le Pi, on sort la carte, on la met dans
l'ordinateur, on copie les animations dans le dossier `animations` du
lecteur nommé **`bootfs`**, on remet la carte. Voir
[MODE_VITRINE.md](MODE_VITRINE.md#3-changer-les-animations).
- **Par le réseau** : voir [MODE_VITRINE.md](MODE_VITRINE.md#3-changer-les-animations).
Pour **retirer** une animation de la boucle sans la supprimer : déplacez-la
dans un sous-dossier dont le nom commence par un point, par exemple
`.archives` (les noms en « . » sont ignorés).
---
## 9. Conseils de composition pour une vitrine
- **Lisibilité depuis la rue** : à 3 mètres, un pixel isolé est un point
lumineux. Les formes de 3 pixels et plus se lisent bien. Un contraste fort
(couleur vive sur noir) se lit de loin.
- **Le mouvement attire l'œil** : une animation lente et régulière se regarde
longtemps ; une image fixe passe inaperçue.
- **Texte** : 5 lettres visibles à la fois. Un défilement d'**1 pixel toutes
les 50 à 80 ms** se lit confortablement. Plus vite, on ne lit plus.
- **Boucles** : soignez la jonction fin → début. Un saut se remarque
beaucoup plus sur 256 pixels que sur un écran.
- **Diffuseur** : si la matrice est placée derrière un matériau diffusant
(calque, plexiglas dépoli), les pixels se fondent et les détails d'un pixel
disparaissent. Testez avec le diffuseur réel.
- **Variété** : alternez animations chargées et calmes, couleurs chaudes et
froides. L'ordre des numéros, c'est votre montage.
### ⚠️ Sécurité : pas de flashs rapides
Une vitrine est vue par **tout le monde**, y compris des personnes
**épileptiques photosensibles**. Les règles d'accessibilité (WCAG) demandent :
- **pas plus de 3 flashs par seconde** (un flash = passage brusque du sombre
au clair puis retour) ;
- surtout **pas de grands aplats rouges** qui clignotent ;
- pas de clignotement de toute la matrice à la fois.
Un stroboscope est très efficace pour attirer l'œil… et peut provoquer une
crise. On s'en passe.
---
## 10. Liste de contrôle avant de rendre une animation
- [ ] Document en **32 × 8** pixels (ou 39 × 7 pour la matrice finale)
- [ ] Pas de flou : **moins de 64 couleurs** (vérificateur)
- [ ] Pas de couleurs **trop sombres** (vérificateur)
- [ ] Fond **noir** ou transparent
- [ ] Vitesse choisie : `images_par_seconde.txt` présent (séquence) ou GIF
- [ ] La boucle s'enchaîne bien (simulateur)
- [ ] **Pas plus de 3 flashs par seconde**
- [ ] Nom de fichier : `NN_nom_sans_accent`
- [ ] Testé sur la vraie matrice au moins une fois
+264
View File
@@ -0,0 +1,264 @@
# Préparer le Raspberry Pi, pas à pas
Ce guide part d'une carte SD vide et va jusqu'à la matrice qui s'allume.
Comptez **une heure** la première fois, dont beaucoup d'attente.
> **Système plus ancien ?** Le projet fonctionne aussi sur **Raspbian 11
> « Bullseye »** (cartes préparées avant fin 2023, testé sur un Pi 3 Model B).
> Seule différence : la partie de la carte SD lisible par tous les
> ordinateurs s'appelle **`/boot`** au lieu de **`/boot/firmware`**. Partout
> où ce guide écrit `/boot/firmware`, lisez `/boot`. Le script d'installation
> et le lecteur s'en rendent compte tout seuls.
On utilise le Pi **« sans écran »** (on dit *headless*) : pas de clavier, pas
d'écran, pas de souris. On lui parle **depuis son ordinateur portable**, par
le Wi-Fi. C'est comme ça qu'il vivra dans la vitrine.
---
## Sommaire
1. [Ce qu'il faut](#1-ce-quil-faut)
2. [Installer le système sur la carte SD](#2-installer-le-système-sur-la-carte-sd)
3. [Premier démarrage](#3-premier-démarrage)
4. [Se connecter au Pi (SSH)](#4-se-connecter-au-pi-ssh)
5. [Copier le projet sur le Pi](#5-copier-le-projet-sur-le-pi)
6. [Installation automatique](#6-installation-automatique)
7. [Ce que fait l'installation (pour comprendre)](#7-ce-que-fait-linstallation-pour-comprendre)
8. [Tester](#8-tester)
9. [Les commandes de survie du terminal](#9-les-commandes-de-survie-du-terminal)
10. [Problèmes fréquents](#10-problèmes-fréquents)
---
## 1. Ce qu'il faut
- Un Raspberry Pi **3B+, 4 ou Zero 2 W** et son bloc secteur.
- Une carte micro-SD de 16 Go ou plus, et un lecteur de carte pour l'ordinateur.
- Un ordinateur (Windows, macOS ou Linux) **sur le même réseau Wi-Fi** que le Pi.
- Le nom et le mot de passe de ce réseau Wi-Fi.
> **Réseau de l'école** : les réseaux avec identifiant personnel (type
> *eduroam*) sont compliqués à configurer sur un Pi. Le plus simple : utiliser
> le **partage de connexion d'un téléphone** (point d'accès), auquel on
> connecte le Pi **et** l'ordinateur. Notez son nom et son mot de passe.
> Attention : le Pi 3B+ et le Zero 2 W ne voient que le Wi-Fi **2,4 GHz**
> (sur iPhone : activer « Maximiser la compatibilité »).
---
## 2. Installer le système sur la carte SD
On utilise le logiciel officiel **Raspberry Pi Imager**.
1. Téléchargez-le et installez-le : <https://www.raspberrypi.com/software/>
2. Insérez la carte SD dans l'ordinateur et lancez Raspberry Pi Imager.
3. **Modèle** : choisissez votre Raspberry Pi.
4. **Système d'exploitation** : *Raspberry Pi OS (other)* →
**Raspberry Pi OS Lite (64-bit)**.
« Lite » = sans bureau graphique : plus léger, démarre plus vite, et on
n'a pas d'écran de toute façon.
5. **Stockage** : votre carte SD. ⚠️ **Tout son contenu sera effacé.**
6. Quand Imager propose de **personnaliser** le système (*customisation*,
*Modifier réglages*), dites **oui** et remplissez :
| Réglage | Valeur conseillée | Pourquoi |
|---|---|---|
| Nom d'hôte (*hostname*) | `vitrine` | le nom du Pi sur le réseau : on le trouvera à l'adresse `vitrine.local` |
| Nom d'utilisateur | `pixel` (par exemple) | votre compte sur le Pi |
| Mot de passe | à choisir, **à noter** | |
| Wi-Fi : nom (SSID) et mot de passe | ceux du réseau ou du téléphone | pour que le Pi se connecte tout seul |
| Pays du Wi-Fi | `FR` | obligatoire pour activer le Wi-Fi |
| Fuseau horaire / clavier | `Europe/Paris` / `fr` | pour que l'heure soit juste |
| **SSH** (onglet *Services*) | **activer**, avec mot de passe | pour se connecter depuis l'ordinateur |
7. Lancez l'écriture et patientez (5 à 15 minutes). Imager vérifie la carte
à la fin, puis l'éjecte.
> **Si vous avez plusieurs Pi**, donnez-leur des noms différents
> (`vitrine1`, `vitrine2`…), sinon ils se gêneront sur le réseau.
---
## 3. Premier démarrage
1. Insérez la carte SD dans le Pi (contacts vers la carte).
2. Branchez le bloc secteur. La LED rouge s'allume, la verte clignote :
le Pi travaille.
3. **Attendez 3 à 5 minutes** : au premier démarrage, le Pi s'installe,
redémarre une ou deux fois, puis se connecte au Wi-Fi.
Pas besoin de brancher la matrice pour l'instant.
---
## 4. Se connecter au Pi (SSH)
**SSH** permet de taper des commandes sur le Pi depuis son ordinateur, comme
si on avait un clavier branché dessus.
Ouvrez un **terminal** :
- **Windows** : menu Démarrer → tapez `PowerShell` → Entrée ;
- **macOS** : Applications → Utilitaires → **Terminal** ;
- **Linux** : le terminal habituel.
Tapez (en remplaçant `pixel` par votre nom d'utilisateur) :
```bash
ssh pixel@vitrine.local
```
- La première fois, il demande si vous faites confiance à cet ordinateur :
tapez `yes` puis Entrée.
- Puis votre **mot de passe**. ⚠️ **Rien ne s'affiche quand on le tape**, pas
même des étoiles : c'est normal. Tapez-le et faites Entrée.
Vous êtes connecté quand la ligne commence par `pixel@vitrine:~ $`.
Tout ce que vous tapez maintenant s'exécute **sur le Pi**.
Pour se déconnecter : `exit`.
---
## 5. Copier le projet sur le Pi
Ouvrez un **deuxième terminal sur votre ordinateur** (pas dans la session
SSH), placez-vous dans le dossier qui contient `8x32 PI`, puis :
```bash
scp -r "8x32 PI" pixel@vitrine.local:~/vitrine
```
`scp` = copie par SSH. Le dossier s'appellera simplement `vitrine` sur le Pi
(sans espace dans le nom, c'est plus pratique dans un terminal).
> **Plutôt à la souris ?** Les logiciels gratuits **WinSCP** (Windows),
> **Cyberduck** (macOS) ou **FileZilla** (tous) permettent de glisser-déposer
> des fichiers sur le Pi. Choisissez le protocole **SFTP**, serveur
> `vitrine.local`, votre nom d'utilisateur et votre mot de passe.
---
## 6. Installation automatique
Dans la **session SSH** (sur le Pi) :
```bash
cd ~/vitrine
```
```bash
bash installation/installer.sh
```
Le script installe tout ce qu'il faut (5 à 15 minutes). Il demande parfois
votre mot de passe (pour `sudo`). À la fin, **redémarrez le Pi** :
```bash
sudo reboot
```
La connexion SSH se coupe. Attendez une minute et reconnectez-vous
(`ssh pixel@vitrine.local`).
---
## 7. Ce que fait l'installation (pour comprendre)
Le script [`installation/installer.sh`](installation/installer.sh) est
commenté ligne par ligne. Voici ses 5 étapes, et les commandes qu'on taperait
à la main :
**1. Installer des logiciels du système.** `apt` est le « magasin
d'applications » de Raspberry Pi OS :
```bash
sudo apt-get install -y python3-venv python3-pil python3-dev gcc
```
**2. Créer un environnement Python** et y installer la bibliothèque qui
parle aux LED. Un *environnement virtuel* est une boîte réservée au projet :
on peut y installer des bibliothèques sans risquer d'abîmer le système.
```bash
python3 -m venv --system-site-packages ~/vitrine-env
```
```bash
~/vitrine-env/bin/pip install rpi_ws281x
```
C'est pour ça qu'on lance ensuite les programmes avec `~/vitrine-env/bin/python`
et pas simplement `python3` : seul ce Python-là connaît `rpi_ws281x`.
**3. Désactiver la sortie son.** Sur le Pi, le son analogique et le signal des
LED utilisent le **même circuit interne** (le générateur « PWM »). On ne peut
pas avoir les deux. Dans le fichier de réglages `/boot/firmware/config.txt`,
la ligne `dtparam=audio=on` devient `dtparam=audio=off`.
**4. Créer le dossier des animations** `/boot/firmware/animations` et y
copier les exemples. Pourquoi là ? Voir [MODE_VITRINE.md](MODE_VITRINE.md) :
c'est la partie de la carte SD que **tous les ordinateurs** savent ouvrir.
**5. Installer le service** de démarrage automatique (sans l'activer).
Voir [MODE_VITRINE.md](MODE_VITRINE.md).
---
## 8. Tester
Branchez la matrice (voir [README, partie 6](README.md#6-brancher-étape-par-étape)),
**alimentation d'établi allumée**, puis dans la session SSH :
```bash
cd ~/vitrine
```
```bash
sudo ~/vitrine-env/bin/python test_matrice.py
```
Puis, si le test est bon :
```bash
sudo ~/vitrine-env/bin/python lecteur.py
```
**Ctrl + C** pour arrêter. Pour que le lecteur démarre tout seul à chaque
allumage, voir [MODE_VITRINE.md](MODE_VITRINE.md).
---
## 9. Les commandes de survie du terminal
| Commande | Effet |
|---|---|
| `pwd` | « où suis-je ? » : affiche le dossier courant |
| `ls` | liste les fichiers du dossier courant |
| `cd vitrine` | entre dans le dossier `vitrine` |
| `cd ..` | remonte d'un dossier |
| `cd ~` | revient au dossier personnel |
| `nano matrice.py` | ouvre un fichier dans un éditeur de texte simple (**Ctrl + O** puis Entrée : enregistrer ; **Ctrl + X** : quitter) |
| **Ctrl + C** | arrête le programme en cours |
| **Tab** | complète automatiquement un nom de fichier (gain de temps énorme !) |
| **↑** | rappelle la commande précédente |
| `sudo reboot` | redémarre le Pi |
| `sudo poweroff` | éteint proprement le Pi (attendre que la LED verte ne clignote plus avant de débrancher) |
| `exit` | quitte la session SSH |
---
## 10. Problèmes fréquents
| Symptôme | Cause probable | Solution |
|---|---|---|
| `ssh: Could not resolve hostname vitrine.local` | Le Pi n'a pas encore fini de démarrer | Attendre 2 minutes de plus |
| | Pi et ordinateur sur deux réseaux différents | Les connecter au même Wi-Fi |
| | Réseau qui bloque les noms `.local` (fréquent à l'école) | Trouver l'**adresse IP** du Pi (dans l'interface du téléphone en point d'accès, liste des appareils connectés) et faire `ssh pixel@192.168.x.x` |
| | Mauvais Wi-Fi saisi dans Imager | Refaire la carte SD (partie 2) |
| `Permission denied, please try again.` | Mauvais mot de passe ou nom d'utilisateur | Retaper ; attention au clavier (QWERTY / AZERTY) |
| `WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!` | La carte SD a été refaite : le Pi a une nouvelle « empreinte » | `ssh-keygen -R vitrine.local`, puis se reconnecter |
| `externally-managed-environment` lors d'un `pip install` | Installation hors de l'environnement virtuel | Utiliser `~/vitrine-env/bin/pip`, pas `pip` |
| Erreur de compilation pendant l'installation de `rpi_ws281x` | Outils de compilation manquants | `sudo apt-get install -y python3-dev gcc`, puis relancer le script |
| `config.txt introuvable` | Système qui n'est pas Raspberry Pi OS | Refaire la carte SD avec Raspberry Pi OS |
+269
View File
@@ -0,0 +1,269 @@
# Mode vitrine : une matrice qui tourne toute seule
Dans la vitrine, personne ne tapera de commande. Il faut que le Pi :
1. **démarre le lecteur tout seul** quand on l'allume ;
2. permette de **changer les animations** facilement ;
3. **survive aux coupures de courant** (une vitrine est souvent éteinte le
soir par un interrupteur général) ;
4. respecte des **horaires**, si besoin.
Ce guide suppose que l'installation est faite ([INSTALLATION_PI.md](INSTALLATION_PI.md))
et que `lecteur.py` fonctionne quand on le lance à la main.
---
## Sommaire
1. [Démarrage automatique](#1-démarrage-automatique)
2. [Surveiller et arrêter le lecteur](#2-surveiller-et-arrêter-le-lecteur)
3. [Changer les animations](#3-changer-les-animations)
4. [Survivre aux coupures de courant](#4-survivre-aux-coupures-de-courant)
5. [Horaires d'allumage](#5-horaires-dallumage)
6. [Alimentation et installation définitive](#6-alimentation-et-installation-définitive)
7. [Entretien et sauvegarde](#7-entretien-et-sauvegarde)
8. [En cas de panne dans la vitrine](#8-en-cas-de-panne-dans-la-vitrine)
---
## 1. Démarrage automatique
L'installation a préparé un **service** nommé `vitrine-pixels` : un programme
que Linux lance tout seul à chaque démarrage, et **relance s'il s'arrête**
(erreur, fichier abîmé…). Son fichier modèle :
[`installation/vitrine-pixels.service`](installation/vitrine-pixels.service).
Pour l'**activer** (une seule fois) :
```bash
sudo systemctl enable --now vitrine-pixels
```
`enable` = démarrer à chaque allumage ; `--now` = et aussi tout de suite.
Débranchez et rebranchez le Pi : environ **30 secondes** après, les
animations démarrent.
Pour le **désactiver** (retour au lancement à la main) :
```bash
sudo systemctl disable --now vitrine-pixels
```
---
## 2. Surveiller et arrêter le lecteur
| Je veux… | Commande |
|---|---|
| savoir si le lecteur tourne | `systemctl status vitrine-pixels` (`active (running)` = oui ; **q** pour quitter) |
| voir ce qu'il affiche, en direct | `journalctl -u vitrine-pixels -f` (**Ctrl + C** pour quitter) |
| l'arrêter jusqu'au prochain démarrage | `sudo systemctl stop vitrine-pixels` |
| le relancer (après avoir modifié un réglage) | `sudo systemctl restart vitrine-pixels` |
> ⚠️ **Un seul programme à la fois** peut parler à la matrice. Avant de lancer
> `test_matrice.py`, `mes_animations.py` ou `lecteur.py` à la main, **arrêtez
> le service** (`sudo systemctl stop vitrine-pixels`), sinon les deux
> programmes se battent et la matrice affiche n'importe quoi.
---
## 3. Changer les animations
Le lecteur lit le dossier **`/boot/firmware/animations`** (ou
**`/boot/animations`** sur les systèmes plus anciens comme Raspbian 11). Ce dossier se trouve
sur la petite partie de la carte SD nommée **`bootfs`** (**`boot`** sur les
systèmes plus anciens), formatée en FAT32 :
le format des clés USB, que **Windows, macOS et Linux** savent tous lire et
écrire. C'est pour ça qu'on l'a choisi.
Le dossier est **relu à chaque tour de boucle** : pas besoin de redémarrer
le lecteur après un changement.
### Méthode 1 : la carte SD (sans réseau, sans terminal)
C'est la méthode à donner à l'équipe design.
1. Éteignez le Pi **proprement** : `sudo poweroff` en SSH, ou… si le Pi est
protégé par l'*overlay* (partie 4), on peut simplement couper le courant.
2. Retirez la carte micro-SD du Pi et mettez-la dans l'ordinateur.
3. Un lecteur nommé **`bootfs`** apparaît. Ouvrez le dossier **`animations`**.
4. Ajoutez, supprimez ou renommez des animations (voir
[GUIDE_KRITA.md, partie 8](GUIDE_KRITA.md#8-déposer-lanimation)).
⚠️ **Ne touchez à rien d'autre** sur `bootfs` : ce sont les fichiers de
démarrage du Pi.
5. **Éjectez** la carte proprement (clic droit → Éjecter), remettez-la dans
le Pi, rallumez.
> **Windows propose de « formater » la carte ?** C'est l'autre partie de la
> carte (le système Linux), que Windows ne sait pas lire. Répondez
> **Annuler**, jamais Formater !
> **macOS** crée des fichiers cachés (`._coeur.gif`, `.DS_Store`) : le
> lecteur les ignore, pas d'inquiétude.
### Méthode 2 : par le réseau
Depuis l'ordinateur, sur le même Wi-Fi que le Pi. Le dossier appartient à
l'administrateur : on copie d'abord dans le dossier personnel, puis on
déplace avec `sudo`.
```bash
scp -r 05_feu_de_camp 06_logo.png pixel@vitrine.local:~/
```
Puis, dans une session SSH sur le Pi :
```bash
sudo mv ~/05_feu_de_camp ~/06_logo.png /boot/firmware/animations/
```
Pour voir ce qu'il y a dans le dossier :
```bash
ls /boot/firmware/animations
```
Pour retirer une animation :
```bash
sudo rm -r /boot/firmware/animations/05_feu_de_camp
```
> Cette méthode ne marche que si la partition `bootfs` n'est **pas protégée
> en écriture** (partie 4).
---
## 4. Survivre aux coupures de courant
### Le problème
Une carte SD n'aime pas qu'on lui coupe le courant **pendant qu'elle écrit**.
Or Linux écrit régulièrement sur la carte (journaux, fichiers temporaires…).
Une coupure au mauvais moment peut abîmer des fichiers, et un jour le Pi ne
redémarre plus. Pour un ordinateur qu'on éteint proprement, aucun souci ;
pour une vitrine coupée chaque soir à l'interrupteur, c'est une question de
semaines.
### La solution : l'« overlay » (système en lecture seule)
Raspberry Pi OS sait rendre la carte SD **intouchable** : toutes les écritures
vont dans la mémoire vive (la RAM), et disparaissent à l'extinction. La carte
SD n'est plus jamais modifiée : on peut couper le courant n'importe quand.
C'est comme une **ardoise magique** posée sur la carte : on peut écrire
dessus, mais tout s'efface au redémarrage.
**Pour l'activer**, en SSH :
```bash
sudo raspi-config
```
1. Avec les flèches : **Performance Options** → **Overlay File System**.
2. « Would you like the overlay file system to be enabled? » → **Oui**.
3. « Would you like the boot partition to be write-protected? » :
- **Oui** (conseillé pour la vitrine) : protection totale ; on change
alors les animations par la **méthode 1** (carte SD dans l'ordinateur),
qui fonctionne toujours ;
- **Non** : la méthode 2 (réseau) reste possible.
4. **Finish**, puis redémarrer.
### ⚠️ Le piège de l'ardoise magique
Avec l'overlay activé, **toute modification faite sur le Pi est perdue au
redémarrage** : un réglage changé dans `matrice.py`, un fichier copié dans
`~/vitrine`… Pour modifier durablement quelque chose :
1. `sudo raspi-config` → désactiver l'Overlay File System → redémarrer ;
2. faire ses modifications, tester ;
3. réactiver l'overlay → redémarrer.
**Règle simple** : on règle tout (orientation, luminosité, horaires) **avant**
d'activer l'overlay, au moment de l'installation dans la vitrine.
---
## 5. Horaires d'allumage
Deux façons de faire :
### A. Une prise programmable (conseillé)
Une **prise minuteur** (mécanique ou numérique, quelques euros) coupe le
courant du Pi **et** de l'alimentation des LED la nuit. Simple, fiable,
économe : la nuit, rien ne consomme. **Il faut alors l'overlay** (partie 4),
puisque le courant est coupé brutalement chaque soir.
### B. Le réglage `HORAIRES` du lecteur
En haut de [`lecteur.py`](lecteur.py) :
```python
HORAIRES = ("09:00", "22:30")
```
En dehors de ces heures, la matrice s'éteint (le Pi, lui, reste allumé).
Les horaires qui passent minuit fonctionnent : `("18:00", "02:00")`.
> ⚠️ **Le Pi n'a pas d'horloge à pile** (sauf le Pi 5). Il met son heure à
> jour **par Internet** au démarrage. Sans réseau dans la vitrine, son heure
> est fausse et les horaires ne marchent pas. Dans ce cas : prise
> programmable.
---
## 6. Alimentation et installation définitive
L'alimentation d'établi, c'est pour l'atelier. Dans la vitrine :
- **Alimentation 5 V fixe** pour les LED, d'au moins **5 A** pour la matrice
8 × 32 à luminosité modérée, **10 A** pour la matrice finale 7 × 39. Préférez
un **bloc secteur fermé** (comme un chargeur d'ordinateur, avec une prise
jack) à une alimentation « nue » à vis : sur ces dernières, le 230 V est
accessible.
⚠️ **Tout câblage en 230 V doit être fait ou vérifié par une personne
compétente.**
- Réglez `COURANT_MAX_MA` dans `matrice.py` à environ **80 %** de ce que
fournit l'alimentation (4000 pour un bloc de 5 A).
- **Luminosité** : en vitrine, en plein jour, il faut souvent monter
`LUMINOSITE` à 100, voire 150. Regardez depuis la rue, à différentes heures.
Plus de luminosité = plus de courant : vérifiez avec `verifier_animation.py`.
- **Fils** : remplacez la plaque d'essai par une **plaque à pastilles soudée**
(ou au minimum, collez les fils au pistolet à colle) : les vibrations et les
manipulations finissent toujours par débrancher un fil Dupont.
- **Chaleur** : derrière une vitre au soleil, il peut faire plus de 50 °C.
Laissez de l'air autour du Pi et de l'alimentation. Pour voir la
température du Pi : `vcgencmd measure_temp` (au-delà de 80 °C, il ralentit).
- **Wi-Fi** : si la vitrine a du Wi-Fi, on peut garder un accès SSH pour
dépanner à distance. Sinon, la méthode « carte SD » suffit.
---
## 7. Entretien et sauvegarde
- **Gardez une carte SD de secours** prête, identique à celle de la vitrine
(même installation). En cas de panne, on échange les cartes en une minute.
- **Gardez une copie des animations** ailleurs que sur la carte SD (sur un
ordinateur ou un disque partagé de l'équipe).
- Pour faire une **copie exacte** d'une carte SD (une « image ») : logiciels
**Win32 Disk Imager** (Windows), **ApplePi-Baker** (macOS), ou la commande
`dd` (Linux). On peut ensuite réécrire cette image sur une nouvelle carte
avec Raspberry Pi Imager (*Use custom*).
---
## 8. En cas de panne dans la vitrine
| Symptôme | Vérifier | Solution |
|---|---|---|
| Rien ne s'allume du tout | La LED rouge du Pi est-elle allumée ? | Prise, prise programmable, bloc secteur du Pi |
| | L'alimentation des LED est-elle branchée ? | La puce 74AHCT125 est alimentée par elle : sans elle, aucun signal |
| Le Pi est allumé mais la matrice reste noire | Le service tourne-t-il ? `systemctl status vitrine-pixels` | `sudo systemctl restart vitrine-pixels` ; lire le journal (partie 2) |
| | Les horaires sont-ils corrects ? L'heure du Pi est-elle bonne ? (`date`) | Corriger `HORAIRES` ou mettre `None` |
| | Le dossier d'animations est-il vide ? | Un point bleu-vert qui respire en bas à droite = « je n'ai rien à jouer » |
| Une animation est sautée | Journal : `✗ impossible de lire ce fichier` | Réexporter le fichier depuis Krita |
| Les nouvelles animations n'apparaissent pas | Ont-elles été copiées dans `bootfs/animations` (et pas ailleurs) ? | Refaire la copie, éjecter proprement la carte |
| | Overlay activé et copie faite par le réseau ? | Avec l'overlay, la copie réseau est perdue au redémarrage : passer par la carte SD |
| Le Pi ne démarre plus (LED verte immobile) | Carte SD abîmée | Remettre la carte de secours ; refaire une carte (et activer l'overlay !) |
| Couleurs aléatoires après un transport | Un fil s'est débranché ou desserré | Revoir le montage avec le [README](README.md#6-brancher-étape-par-étape) |
+446
View File
@@ -0,0 +1,446 @@
# Matrice LED 8 × 32 (WS2812B) avec un Raspberry Pi
Workshop « Vitrine pixels » : la même matrice de **256 pixels** que dans le
dossier [`matrice_8x32`](../matrice_8x32/README.md), mais pilotée par un
**Raspberry Pi**. L'avantage : le Pi sait **lire des images**. Les animations
dessinées dans **Krita** sont jouées telles quelles, sans les traduire en code.
![De Krita à la matrice](schemas/chaine_krita_matrice.svg)
> **À qui s'adresse ce dossier ?**
> - **L'équipe électronique** : ce README (montage, test, dépannage), puis
> [INSTALLATION_PI.md](INSTALLATION_PI.md) (préparer le Pi).
> - **L'équipe design** : [GUIDE_KRITA.md](GUIDE_KRITA.md) (dessiner et exporter
> pour la matrice). Pas besoin de toucher au matériel !
> - **Pour la vitrine** : [MODE_VITRINE.md](MODE_VITRINE.md) (démarrage
> automatique, changer les animations, coupures de courant).
> - **Pour qui veut coder** : [CODER_EN_PYTHON.md](CODER_EN_PYTHON.md).
---
## Sommaire
1. [Arduino ou Raspberry Pi ?](#1-arduino-ou-raspberry-pi-)
2. [Le matériel](#2-le-matériel)
3. [Comment ça marche ?](#3-comment-ça-marche-)
4. [Le problème des 3,3 V et le 74AHCT125](#4-le-problème-des-33-v-et-le-74ahct125)
5. [Les 5 règles d'or](#5-les-5-règles-dor)
6. [Brancher, étape par étape](#6-brancher-étape-par-étape)
7. [Préparer le Raspberry Pi](#7-préparer-le-raspberry-pi)
8. [Premier allumage : le test](#8-premier-allumage--le-test)
9. [Jouer les animations](#9-jouer-les-animations)
10. [Ça ne marche pas ?](#10-ça-ne-marche-pas-)
11. [Petit lexique](#11-petit-lexique)
12. [Les fichiers du dossier](#12-les-fichiers-du-dossier)
---
## 1. Arduino ou Raspberry Pi ?
Ce sont deux cartes très différentes :
- L'**Arduino** est un **microcontrôleur** : une petite puce qui exécute un
seul programme, en boucle, dès qu'on l'alimente. Il est simple et robuste,
mais minuscule : 2 Ko de mémoire de travail et 32 Ko pour le programme.
- Le **Raspberry Pi** est un vrai **ordinateur**, de la taille d'une carte de
crédit, avec un système (Linux), des fichiers, le Wi-Fi, une carte SD de
plusieurs gigaoctets…
| | Arduino Uno | Raspberry Pi |
|---|---|---|
| Passer d'un dessin Krita à la matrice | dessin → script de conversion → code C → téléversement | copier le GIF dans un dossier |
| Nombre d'images possibles | ≈ 30 (une image = 768 octets, la mémoire est pleine très vite) | des milliers (carte SD) |
| Changer les animations | ordinateur + câble USB + IDE Arduino | carte SD, ou par le réseau |
| Démarrage | instantané | ≈ 30 secondes |
| Coupure de courant brutale | aucun problème | risque d'abîmer la carte SD (solution : [MODE_VITRINE.md](MODE_VITRINE.md)) |
| Langage | C++ | Python |
| Niveau électrique de la broche de données | 5 V : parfait pour les LED | **3,3 V : trop faible**, il faut un adaptateur (partie 4) |
**Conclusion** : pour **apprendre** (comprendre les bits, le multiplexage, la
chaîne de LED), l'Arduino est idéal. Pour **diffuser les créations d'une
équipe design** dans une vitrine, le Raspberry Pi est beaucoup plus pratique.
---
## 2. Le matériel
| Quoi | Combien | Remarque |
|---|---|---|
| Matrice LED WS2812B 8 × 32, souple | 1 | la même que dans `matrice_8x32` |
| **Raspberry Pi 3B+, 4 ou Zero 2 W** | 1 | ⚠️ **pas le Pi 5** (voir encadré) |
| Bloc secteur USB **officiel** du Pi | 1 | micro-USB 2,5 A (3B+, Zero 2 W) ou USB-C 3 A (Pi 4) |
| Carte micro-SD 16 Go ou plus | 1 | « classe 10 » / « A1 » |
| Puce **74AHCT125** (boîtier DIP 14 pattes) | 1 | l'adaptateur 3,3 V → 5 V (partie 4). Le **74HCT125** convient aussi |
| Alimentation d'établi | 1 | réglée sur **5,0 V**, limite ≈ 3 A |
| 2 cordons banane → pince crocodile | 1 rouge + 1 noir | |
| Résistance **330 Ω** | 1 | orange-orange-marron |
| Condensateur électrolytique **1000 µF**, 10 V ou plus | 1 | |
| Plaque d'essai (breadboard) | 1 | obligatoire cette fois, pour la puce |
| Fils Dupont **mâle-mâle** | 6 | sur la plaque d'essai |
| Fils Dupont **femelle-mâle** | 2 | du connecteur du Pi vers la plaque d'essai |
| Lecteur de carte SD pour l'ordinateur | 1 | pour préparer la carte |
> **Pourquoi pas le Raspberry Pi 5 ?** Sur le Pi 5, les broches sont gérées
> par une nouvelle puce (« RP1 »), et la bibliothèque `rpi_ws281x` que nous
> utilisons ne sait pas encore lui parler. Il existe d'autres bibliothèques
> pour le Pi 5, mais elles sont moins simples. Le Pi 4, le Pi 3B+ et le petit
> Pi Zero 2 W (≈ 20 €, largement assez puissant) fonctionnent parfaitement.
---
## 3. Comment ça marche ?
Côté **matrice**, rien ne change par rapport à l'Arduino (relisez la
[partie 2 du README Arduino](../matrice_8x32/README.md#2-comment-ça-marche-)) :
les 256 LED forment une **chaîne**, et on leur envoie une longue liste de
couleurs par **un seul fil** de données. L'énergie vient de l'alimentation
d'établi.
Côté **cerveau**, le Raspberry Pi fait tourner le programme
[`lecteur.py`](lecteur.py), qui :
1. regarde ce qu'il y a dans le dossier `animations` ;
2. ouvre chaque fichier (GIF, PNG…) et le découpe en images de 32 × 8 ;
3. pour chaque image, **corrige les couleurs** pour que la matrice ressemble
à l'écran de Krita (la « correction gamma »), applique la luminosité et la
limite de courant ;
4. remet les pixels dans l'**ordre de la chaîne** (le serpentin, avec la
même fonction `XY()` que sur l'Arduino) ;
5. envoie les 256 couleurs dans le fil, attend la durée prévue, passe à
l'image suivante ;
6. recommence avec l'animation suivante, et ainsi de suite, en boucle.
---
## 4. Le problème des 3,3 V et le 74AHCT125
### Le problème
Les broches du Raspberry Pi fonctionnent en **3,3 V** : un « 1 » envoyé dans le
fil de données vaut 3,3 V. Mais une LED WS2812B alimentée en 5 V ne comprend
un « 1 » qu'à partir d'environ **3,5 V** (70 % de 5 V).
3,3 V, c'est juste en dessous. Souvent ça marche quand même… et parfois les
couleurs deviennent aléatoires, ou ça scintille, selon la température, la
longueur du fil, le lot de LED. Pour une vitrine qui doit tourner des
semaines, on ne joue pas à pile ou face.
> ⚠️ **Ne jamais relier directement le 5 V à une broche du Pi** : les broches du
> Pi supportent au maximum 3,3 V. Au-delà, elles grillent, et souvent le Pi
> avec. Le courant ne doit circuler que **du Pi vers la matrice**, jamais
> l'inverse.
### La solution : un adaptateur de niveau
Le **74AHCT125** est une petite puce qui « répète » un signal en le
renforçant : elle écoute un 3,3 V faible et ressort un vrai 5 V propre. Elle
contient 4 répéteurs (des « portes »). On n'en utilise qu'un seul.
![Brochage du 74AHCT125](schemas/brochage_74ahct125.svg)
| Patte | Nom | Rôle | Branchée sur |
|---|---|---|---|
| **14** | VCC | alimentation de la puce | **5 V** (fil rouge de la fiche DIN) |
| **7** | GND | masse | **GND** |
| **1** | 1OE | « marche/arrêt » du répéteur n° 1 (actif quand il est à 0 V) | **GND** (= toujours en marche) |
| **2** | 1A | **entrée** du répéteur | **GPIO 18** du Pi (patte 12) |
| **3** | 1Y | **sortie** du répéteur | résistance 330 Ω → fil **vert** (DIN) |
| 4 à 6, 8 à 13 | | répéteurs 2, 3, 4 | rien (on peut relier 4, 10 et 13 au 5 V pour les éteindre) |
**Repérer la patte 1** : le boîtier a une **encoche** (demi-lune) à une
extrémité, et souvent un petit **point** à côté de la patte 1. Encoche à
gauche, la patte 1 est **en bas à gauche**. On compte ensuite dans le sens
inverse des aiguilles d'une montre : 1 à 7 en bas, de gauche à droite, puis
8 à 14 en haut, de droite à gauche.
> **Pourquoi « AHCT » et pas « HC » ?** Les lettres **T** (comme dans HCT,
> AHCT) veulent dire que la puce accepte des entrées « faibles » de 3,3 V même
> quand elle est alimentée en 5 V. Un 74**HC**125, lui, ne serait pas plus
> malin que les LED. Vérifiez bien les lettres écrites sur la puce.
---
## 5. Les 5 règles d'or
1. **5 volts, pas plus.** Au-delà de 5,5 V, les LED grillent. On règle
l'alimentation d'établi **avant** de brancher.
2. **Jamais le 5 V du Pi vers la matrice.** Les pattes 2 et 4 du Pi (5 V)
ne sont reliées à **rien**. La matrice est alimentée **uniquement** par
l'alimentation d'établi.
3. **Jamais de 5 V sur une broche du Pi.** Seule la patte 12 (GPIO 18) et la
patte 6 (GND) sont branchées. Le 5 V arrive à la puce, pas au Pi.
4. **Toutes les masses (GND) reliées ensemble** : Pi, puce, matrice et
alimentation d'établi.
5. **On câble hors tension**, Pi éteint, et on fait vérifier avant d'allumer.
---
## 6. Brancher, étape par étape
![Schéma de branchement](schemas/branchement.svg)
**Tout est débranché** : alimentation d'établi éteinte, Pi sans son bloc
secteur.
### Étape A : l'énergie de la matrice
Exactement comme avec l'Arduino ([README Arduino, parties 5 et 6 A](../matrice_8x32/README.md#5-régler-lalimentation-détabli)) :
1. Réglez l'alimentation d'établi sur **5,0 V**, limite de courant **3 A**,
puis éteignez-la.
2. Cordon **rouge** : borne **+** → fil **rouge** du milieu de la matrice.
3. Cordon **noir** : borne **−** → fil **noir** du milieu.
4. **Condensateur 1000 µF** entre ces deux fils (bande « − » côté noir).
### Étape B : la puce sur la plaque d'essai
5. Enfoncez le **74AHCT125** à cheval sur la **rainure centrale** de la plaque
d'essai, encoche à gauche. (Chaque patte doit avoir sa propre rangée de
trous ; sans la rainure, les pattes d'en face seraient reliées entre elles.)
6. Branchez la **fiche** sur le connecteur **`DIN`** de la matrice.
7. Fil **rouge** de la fiche → rangée de la **patte 14** (VCC).
*Avec l'Arduino, ce fil restait isolé. Ici, il apporte le 5 V de la matrice
à la puce. Une puce consomme à peine quelques milliampères : aucun risque.*
8. Fil **blanc** de la fiche → une **ligne de masse** de la plaque d'essai
(la bande bleue « − » sur le côté).
9. Patte **7** (GND) → ligne de masse (fil mâle-mâle).
10. Patte **1** (1OE) → ligne de masse (fil mâle-mâle).
11. **Résistance 330 Ω** : une patte dans la rangée de la **patte 3** (1Y),
l'autre dans une rangée libre.
12. Fil **vert** de la fiche → la rangée libre de la résistance.
### Étape C : le Raspberry Pi
Le connecteur du Pi a **40 pattes** sur deux rangées. La **patte 1** est celle
qui a une **pastille carrée** au dos du circuit, dans le coin. Les pattes
**impaires** (1, 3, 5…) sont sur la rangée intérieure, les **paires** (2, 4,
6…) sur la rangée du bord.
```
bord de la carte
┌──────────────────────────────────────
│ 2 4 6 8 10 12 14 … 40 ← rangée du bord (paires)
│ 5V 5V GND GPIO18
│ 1 3 5 7 9 11 13 … 39 ← rangée intérieure (impaires)
│ 3V3
```
13. Fil femelle-mâle : patte **6** (GND) du Pi → ligne de masse de la plaque.
14. Fil femelle-mâle : patte **12** (GPIO 18) du Pi → rangée de la **patte 2**
(1A) de la puce.
### Étape D : vérifier
- [ ] Alimentation d'établi réglée sur 5,0 V, limite de courant réglée
- [ ] + rouge → fil rouge du milieu ; − noir → fil noir du milieu ; condensateur dans le bon sens
- [ ] Fiche sur **`DIN`** (pas `DOUT`)
- [ ] Puce à cheval sur la rainure, **encoche à gauche**
- [ ] Rouge de la fiche → patte 14 ; blanc → masse
- [ ] Pattes 1 et 7 → masse
- [ ] Patte 3 → 330 Ω → vert de la fiche
- [ ] Pi : patte 12 → patte 2 de la puce ; patte 6 → masse
- [ ] **Rien** sur les pattes 1, 2, 4 (3,3 V et 5 V) du Pi
- [ ] Montage vérifié par l'encadrant
### Ordre d'allumage et d'extinction
| Allumer | Éteindre |
|---|---|
| 1. l'alimentation d'établi | 1. arrêter le Pi proprement (`sudo poweroff`), attendre que la LED verte s'éteigne |
| 2. puis le bloc secteur du Pi | 2. débrancher le bloc secteur du Pi |
| | 3. éteindre l'alimentation d'établi |
> **Bonus du montage** : la puce est alimentée **par la matrice** (fil rouge
> de la fiche). Si l'alimentation d'établi est éteinte, la puce l'est aussi et
> ne fait plus passer le signal : la première LED est protégée.
---
## 7. Préparer le Raspberry Pi
C'est l'objet d'un guide à part : **[INSTALLATION_PI.md](INSTALLATION_PI.md)**.
En résumé :
1. installer **Raspberry Pi OS Lite** sur la carte SD avec **Raspberry Pi Imager** ;
2. se connecter au Pi depuis son ordinateur (**SSH**) ;
3. copier ce dossier sur le Pi ;
4. lancer `bash installation/installer.sh`, qui installe tout le reste.
Comptez une heure la première fois.
---
## 8. Premier allumage : le test
Sur le Pi (connecté en SSH), dans le dossier du projet :
```bash
sudo ~/vitrine-env/bin/python test_matrice.py
```
> **Pourquoi `sudo` ?** Pour envoyer un signal aussi rapide et précis
> (800 000 bits par seconde), la bibliothèque utilise directement des
> circuits internes du Pi. Linux réserve cet accès à l'**administrateur** :
> `sudo` veut dire « exécuter en tant qu'administrateur ».
Le test se déroule en trois temps (voir [`test_matrice.py`](test_matrice.py)) :
**1. Les coins** (5 secondes). Posez la matrice **face LED vers vous** :
```
ROUGE ●· · · · · · · · · · · · · · · · ● VERT
· · · · · · · · · · · · · · · · ·
· · · · · · · · · · · · · · · · ·
BLEU ●· · · · · · · · · · · · · · · · ● BLANC
```
| Ce que vous voyez | Ce qu'il faut faire, dans [`matrice.py`](matrice.py) |
|---|---|
| Rouge en haut à gauche, vert en haut à droite | rien, c'est parfait ✅ |
| Rouge et vert inversés (gauche ↔ droite) | `INVERSER_X = True` |
| Rouge et vert en bas | `INVERSER_Y = True` |
| Les deux à la fois | les deux à `True` |
Pour modifier le fichier sur le Pi : `nano matrice.py`, changez la ligne,
puis **Ctrl + O**, **Entrée** (enregistrer) et **Ctrl + X** (quitter).
**2. Le serpentin** : un point orange parcourt les 256 LED dans l'ordre de la
chaîne.
**3. Les couleurs pleines** : rouge, vert, bleu, puis blanc. Si le rouge
apparaît vert, l'ordre des couleurs est faux (voir partie 10).
---
## 9. Jouer les animations
```bash
sudo ~/vitrine-env/bin/python lecteur.py
```
Le lecteur joue en boucle, **par ordre alphabétique**, tout ce qu'il trouve
dans le dossier des animations :
| Format | Exemple | Remarque |
|---|---|---|
| GIF animé | `01_coeur_qui_bat.gif` | le plus simple ; la vitesse est dans le fichier |
| PNG animé (APNG) ou WebP animé | `04_balle.png` | plus de couleurs que le GIF |
| Séquence de PNG | `03_vague/` contenant `vague_0000.png`, `vague_0001.png`… | la vitesse est dans `images_par_seconde.txt` (sinon 12) |
| Image fixe | `logo.png` | affichée 10 secondes |
Le dossier lu est **`/boot/firmware/animations`** s'il existe (créé par
l'installation, voir [MODE_VITRINE.md](MODE_VITRINE.md)), sinon le dossier
[`animations`](animations/) à côté du programme. Il est **relu à chaque tour** :
on peut ajouter un GIF pendant que ça tourne.
Pour arrêter : **Ctrl + C**.
Les réglages sont en haut de [`lecteur.py`](lecteur.py) (durée minimum par
animation, pause entre deux, horaires) et de [`matrice.py`](matrice.py) :
| Réglage | Fichier | Valeur | À quoi ça sert |
|---|---|---|---|
| `LUMINOSITE` | matrice.py | `40` | de 0 à 255 ; montez en plein jour, en vitrine |
| `COURANT_MAX_MA` | matrice.py | `2500` | un peu **sous** la limite de l'alimentation |
| `INVERSER_X`, `INVERSER_Y` | matrice.py | `False` | orientation (partie 8) |
| `GAMMA` | matrice.py | `2.2` | correction des couleurs (1.0 = aucune) |
| `DUREE_MINIMUM` | lecteur.py | `10` | une animation courte est rejouée pendant au moins 10 s |
| `HORAIRES` | lecteur.py | `None` | ex. `("09:00", "22:30")` |
### Sans matériel : le simulateur
Sur **n'importe quel ordinateur** avec Python et Pillow (voir
[GUIDE_KRITA.md, partie 7](GUIDE_KRITA.md#7-prévisualiser-sur-son-ordinateur)) :
```bash
python3 lecteur.py --simulateur
```
Une fenêtre s'ouvre avec 256 ronds de couleur qui imitent la matrice.
---
## 10. Ça ne marche pas ?
| Symptôme | Causes probables | Solution |
|---|---|---|
| **Rien ne s'allume** | Fiche sur `DOUT` au lieu de `DIN` | Changer de côté |
| | Alimentation d'établi éteinte | L'allumer, activer `OUTPUT` (la puce aussi est alimentée par là) |
| | Puce à l'envers | Encoche à gauche, patte 1 en bas à gauche |
| | Fil sur la mauvaise patte du Pi | Patte **12** = GPIO 18 ; recompter depuis la patte 1 |
| | Pas de GND commun | Patte 6 du Pi, patte 7 et patte 1 de la puce, fil blanc : tous sur la même ligne de masse |
| **`ws2811_init failed`** ou **`Permission denied`** | Programme lancé sans `sudo` | Ajouter `sudo` devant la commande |
| **`ModuleNotFoundError: rpi_ws281x`** | Mauvais Python | Utiliser `~/vitrine-env/bin/python`, pas `python3` |
| | Installation incomplète | Relancer `bash installation/installer.sh` |
| **Le simulateur s'ouvre au lieu de la matrice** | Même cause : `rpi_ws281x` introuvable | Idem |
| **Ça marche, puis tout se fige ou scintille quand on branche un casque / lance un son** | La sortie audio utilise le même circuit que GPIO 18 | Vérifier `dtparam=audio=off` dans `/boot/firmware/config.txt`, redémarrer |
| **Couleurs aléatoires, scintillement** | Puce absente ou de type HC (sans T) | Vérifier les lettres sur la puce |
| | Masse mal serrée | Revérifier toutes les masses |
| **Couleurs fausses** (rouge ↔ vert) | Ordre des couleurs | Dans `matrice.py`, remplacer `WS2811_STRIP_GRB` par `WS2811_STRIP_RGB` |
| **Les couleurs sombres de mon dessin n'apparaissent pas** | Normal : correction gamma + luminosité faible | Voir [GUIDE_KRITA.md, partie 5](GUIDE_KRITA.md#5-ce-qui-change-entre-lécran-et-les-led) |
| **Un fichier est sauté** (`✗ impossible de lire`) | Fichier abîmé ou format inconnu (MP4, KRA…) | Réexporter en GIF ou PNG |
| **La matrice devient orangée quand tout s'allume** | L'alimentation atteint sa limite (voyant `CC`) | Baisser `LUMINOSITE` ou `COURANT_MAX_MA` |
| **Le Pi redémarre tout seul, éclair jaune à l'écran** | Bloc secteur du Pi trop faible | Utiliser le bloc officiel |
| **Ça chauffe, ça sent le brûlé** | Court-circuit, condensateur ou puce à l'envers | **Tout couper immédiatement**, appeler l'encadrant |
Pour voir ce que fait le lecteur quand il tourne tout seul (mode vitrine) :
`journalctl -u vitrine-pixels -f` (voir [MODE_VITRINE.md](MODE_VITRINE.md)).
---
## 11. Petit lexique
| Mot | Signification |
|---|---|
| **Raspberry Pi** | Petit ordinateur complet sur une seule carte |
| **Raspberry Pi OS** | Le système d'exploitation du Pi (une version de Linux) |
| **Carte SD** | Le « disque dur » du Pi : système, programmes et animations |
| **GPIO** | *General Purpose Input/Output* : les pattes du Pi qu'on peut commander par programme |
| **3,3 V / 5 V** | Les deux niveaux électriques en jeu : celui du Pi et celui des LED |
| **Adaptateur de niveau** | Puce qui traduit un signal 3,3 V en 5 V (ici le 74AHCT125) |
| **DIP 14** | Boîtier de puce à 14 pattes, en deux rangées, qui s'enfiche dans une plaque d'essai |
| **SSH** | Moyen de « taper des commandes » sur le Pi depuis un autre ordinateur, par le réseau |
| **Terminal** | Fenêtre où l'on tape des commandes au lieu de cliquer |
| **sudo** | « Exécuter en tant qu'administrateur » |
| **Python** | Le langage de programmation utilisé ici |
| **Bibliothèque** | Code écrit par d'autres qu'on réutilise (Pillow pour les images, rpi_ws281x pour les LED) |
| **Environnement virtuel** | Une boîte qui contient les bibliothèques Python du projet (`~/vitrine-env`) |
| **Service** | Programme que Linux lance tout seul au démarrage, et relance s'il s'arrête |
| **Correction gamma** | Calcul qui adapte les couleurs d'un écran à la façon dont les LED émettent la lumière |
| **GIF, APNG, WebP** | Formats d'images animées |
| **Serpentin** | Câblage en zigzag des LED : une colonne descend, la suivante remonte |
---
## 12. Les fichiers du dossier
| Fichier | Pour qui | Contenu |
|---|---|---|
| [README.md](README.md) | électronique | ce document |
| [INSTALLATION_PI.md](INSTALLATION_PI.md) | électronique | préparer la carte SD et le Pi, pas à pas |
| [GUIDE_KRITA.md](GUIDE_KRITA.md) | design | dessiner, exporter, vérifier pour la matrice |
| [MODE_VITRINE.md](MODE_VITRINE.md) | tous | démarrage automatique, changer les animations, entretien |
| [CODER_EN_PYTHON.md](CODER_EN_PYTHON.md) | code | créer des animations en Python ; comparaison avec l'Arduino |
| [`matrice.py`](matrice.py) | | le moteur : réglages, `XY()`, gamma, vraie matrice et simulateur |
| [`lecteur.py`](lecteur.py) | | joue les animations du dossier en boucle |
| [`test_matrice.py`](test_matrice.py) | | test de branchement et d'orientation |
| [`mes_animations.py`](mes_animations.py) | | les animations de l'Arduino, traduites en Python |
| [`animations/`](animations/) | | exemples : GIF, séquence de PNG, APNG |
| `apercus/` | design | créé par `verifier_animation.py` : aperçus « façon LED » à partager |
| [`krita/palette_vitrine_pixels.gpl`](krita/palette_vitrine_pixels.gpl) | design | palette de couleurs adaptée aux LED, à importer dans Krita |
| [`outils/verifier_animation.py`](outils/verifier_animation.py) | design | vérifie un export et crée un aperçu « façon LED » |
| [`outils/generer_exemples.py`](outils/generer_exemples.py) | | recrée les exemples et la palette |
| [`outils/schemas.py`](outils/schemas.py) | | redessine les schémas du dossier `schemas/` |
| [`installation/`](installation/) | électronique | script d'installation et service de démarrage automatique |
---
## Pour aller plus loin
- **La matrice finale 7 × 39** : changez `LARGEUR`, `HAUTEUR` et la fonction
`XY()` dans `matrice.py` ; il faudra une alimentation 5 V de 10 A environ.
Les dessins Krita se feront alors en 39 × 7.
- **Plusieurs matrices** : la bibliothèque sait piloter deux chaînes à la fois
(GPIO 18 et GPIO 13).
- **Interactivité** : le Pi peut lire un bouton, un capteur de présence, une
page web… et changer d'animation en conséquence.
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1 @@
20
Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+100
View File
@@ -0,0 +1,100 @@
#!/bin/bash
# =============================================================================
# INSTALLATION AUTOMATIQUE sur le Raspberry Pi
# Workshop « Vitrine pixels »
# =============================================================================
#
# À lancer SUR LE RASPBERRY PI, depuis le dossier du projet :
# bash installation/installer.sh
#
# Ce script fait, dans l'ordre, tout ce qui est décrit à la main dans
# INSTALLATION_PI.md (parties 5 à 8). Chaque étape affiche ce qu'elle fait.
# On peut le relancer sans danger : ce qui est déjà fait est ignoré.
set -e # s'arrêter à la première erreur, au lieu de continuer à l'aveugle
# Le dossier du projet = le dossier parent de celui qui contient ce script.
PROJET="$(cd "$(dirname "$0")/.." && pwd)"
ENV="$HOME/vitrine-env" # l'« environnement Python » du projet
# La partie « boot » de la carte SD (lisible depuis tout ordinateur) est
# montée dans /boot/firmware depuis Raspberry Pi OS 12 « Bookworm »,
# et dans /boot sur les systèmes plus anciens (Raspbian 11 « Bullseye »).
if [ -f /boot/firmware/config.txt ]; then
BOOT=/boot/firmware
elif [ -f /boot/config.txt ]; then
BOOT=/boot
else
echo "!!! config.txt introuvable : ce script est prévu pour Raspberry Pi OS."
exit 1
fi
CONFIG=$BOOT/config.txt # le fichier de réglages du Pi
ANIMATIONS=$BOOT/animations # les animations, lisibles depuis tout ordinateur
echo "=== Projet : $PROJET"
echo "=== Partition de démarrage : $BOOT"
echo
echo "=== 1/5 Installation des logiciels nécessaires (quelques minutes)"
# python3-pil = Pillow (lire les images) ; python3-dev et gcc = pour compiler
# la bibliothèque rpi_ws281x, qui parle aux LED.
sudo apt-get update
sudo apt-get install -y python3-venv python3-pil python3-dev gcc
echo
echo "=== 2/5 Environnement Python dans $ENV"
# Un « environnement virtuel » est une boîte où l'on installe les
# bibliothèques du projet sans toucher au reste du système.
# --system-site-packages : la boîte peut aussi utiliser Pillow installé par apt.
if [ ! -d "$ENV" ]; then
python3 -m venv --system-site-packages "$ENV"
fi
"$ENV/bin/pip" install --upgrade rpi_ws281x
echo
echo "=== 3/5 Désactivation de la sortie audio (elle utilise le même circuit que les LED)"
if grep -q "^dtparam=audio=on" "$CONFIG"; then
sudo sed -i 's/^dtparam=audio=on/dtparam=audio=off/' "$CONFIG"
echo " audio désactivé (pris en compte au prochain redémarrage)"
REDEMARRER=1
else
echo " déjà fait"
fi
echo
echo "=== 4/5 Dossier des animations : $ANIMATIONS"
# Il est sur la partie « bootfs » de la carte SD, que Windows, macOS et Linux
# savent tous ouvrir : on pourra y copier des GIF depuis n'importe quel ordinateur.
sudo mkdir -p "$ANIMATIONS"
if [ -z "$(ls -A "$ANIMATIONS")" ]; then
sudo cp -r "$PROJET/animations/." "$ANIMATIONS/"
echo " exemples copiés"
else
echo " le dossier contient déjà des animations : on n'y touche pas"
fi
echo
echo "=== 5/5 Service de démarrage automatique « vitrine-pixels »"
# On recopie le modèle de service en remplaçant @PROJET@ et @PYTHON@
# par les vrais chemins.
sed -e "s|@PROJET@|$PROJET|g" -e "s|@PYTHON@|$ENV/bin/python|g" \
"$PROJET/installation/vitrine-pixels.service" \
| sudo tee /etc/systemd/system/vitrine-pixels.service > /dev/null
sudo systemctl daemon-reload
echo " service installé (pas encore activé)"
echo
echo "================================================================"
echo " Installation terminée."
echo
echo " 1. Testez d'abord la matrice :"
echo " sudo $ENV/bin/python $PROJET/test_matrice.py"
echo " 2. Puis le lecteur :"
echo " sudo $ENV/bin/python $PROJET/lecteur.py"
echo " 3. Quand tout marche, démarrage automatique à chaque allumage :"
echo " sudo systemctl enable --now vitrine-pixels"
if [ -n "$REDEMARRER" ]; then
echo
echo " ⚠ Redémarrez d'abord le Pi (sudo reboot) : le réglage audio"
echo " ne sera pris en compte qu'après."
fi
echo "================================================================"
@@ -0,0 +1,34 @@
# =============================================================================
# Service « vitrine-pixels » : lance lecteur.py à chaque démarrage du Pi
# =============================================================================
#
# Un « service » est un programme que Linux démarre tout seul, relance s'il
# plante, et dont il garde le journal. Ce fichier est un MODÈLE :
# installation/installer.sh remplace @PROJET@ et @PYTHON@ par les vrais
# chemins et le copie dans /etc/systemd/system/.
#
# Commandes utiles (voir MODE_VITRINE.md) :
# sudo systemctl enable --now vitrine-pixels activer + démarrer
# sudo systemctl disable --now vitrine-pixels désactiver + arrêter
# sudo systemctl restart vitrine-pixels relancer (après un réglage)
# systemctl status vitrine-pixels est-ce que ça tourne ?
# journalctl -u vitrine-pixels -f voir ce que le lecteur affiche
[Unit]
Description=Vitrine pixels : lecteur d'animations sur la matrice LED
After=local-fs.target
[Service]
Type=simple
ExecStart="@PYTHON@" "@PROJET@/lecteur.py"
WorkingDirectory=@PROJET@
# Le lecteur a besoin des droits d'administrateur pour parler aux LED.
User=root
# S'il s'arrête (erreur, fichier abîmé…), on le relance au bout de 5 s.
Restart=always
RestartSec=5
# Afficher les messages tout de suite dans le journal.
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
+41
View File
@@ -0,0 +1,41 @@
GIMP Palette
Name: Vitrine pixels (LED)
Columns: 8
#
255 0 0 Rouge
255 90 0 Orange
255 200 0 Jaune
0 255 0 Vert
0 255 200 Cyan
0 60 255 Bleu
140 0 255 Violet
255 0 110 Rose
153 0 0 Rouge moyen
153 54 0 Orange moyen
153 120 0 Jaune moyen
0 153 0 Vert moyen
0 153 120 Cyan moyen
0 36 153 Bleu moyen
84 0 153 Violet moyen
153 0 66 Rose moyen
89 0 0 Rouge sombre
89 31 0 Orange sombre
89 70 0 Jaune sombre
0 89 0 Vert sombre
0 89 70 Cyan sombre
0 21 89 Bleu sombre
49 0 89 Violet sombre
89 0 38 Rose sombre
255 140 140 Rouge pastel
255 181 140 Orange pastel
255 230 140 Jaune pastel
140 255 140 Vert pastel
140 255 230 Cyan pastel
140 167 255 Bleu pastel
203 140 255 Violet pastel
255 140 190 Rose pastel
0 0 0 Noir (éteint)
90 90 90 Gris sombre
160 160 160 Gris
255 200 140 Blanc chaud
255 255 255 Blanc
+344
View File
@@ -0,0 +1,344 @@
"""
=============================================================================
LECTEUR D'ANIMATIONS — de Krita à la matrice
Workshop « Vitrine pixels »
=============================================================================
Ce programme joue EN BOUCLE toutes les animations d'un dossier :
- des GIF animés (coeur.gif)
- des PNG animés (APNG) (vague.png)
- des WebP animés (pluie.webp)
- des images fixes (logo.png : affichée quelques secondes)
- des SÉQUENCES d'images (un sous-dossier rempli de PNG numérotés :
vague_0000.png, vague_0001.png…, comme
Krita les exporte)
Elles sont jouées par ordre alphabétique : pour choisir l'ordre,
commencez les noms par un numéro (01_coeur.gif, 02_vague/…).
Le dossier est relu à chaque tour : on peut ajouter ou retirer des
animations sans arrêter le programme.
COMMENT LE LANCER
-----------------
Sur le Raspberry Pi (sudo est obligatoire pour parler à la matrice) :
sudo ~/vitrine-env/bin/python lecteur.py
Sur votre ordinateur, avec le simulateur :
python3 lecteur.py --simulateur
Pour essayer UN SEUL fichier ou dossier :
python3 lecteur.py --simulateur mon_animation.gif
Pour lire un autre dossier :
python3 lecteur.py --dossier /chemin/vers/mes/animations
ORGANISATION DU PROGRAMME
-------------------------
1. Réglages.
2. Trouver les animations dans le dossier.
3. Charger une animation : la découper en images de 32 × 8.
4. Jouer une animation.
5. Les horaires (allumer la vitrine seulement à certaines heures).
6. Le programme principal.
"""
import argparse # pour lire les options (--simulateur, --dossier…)
import datetime # pour connaître l'heure (horaires de la vitrine)
import os # pour parcourir les dossiers et les fichiers
import time
from PIL import Image, ImageSequence
import matrice # notre moteur (fichier matrice.py, dans le même dossier)
from matrice import LARGEUR, HAUTEUR
# =============================================================================
# 1. RÉGLAGES
# =============================================================================
# Où chercher les animations ? On essaie ces dossiers dans l'ordre et on
# prend le premier qui existe.
# - /boot/firmware/animations : sur la carte SD du Pi, dans la partie
# qu'on peut ouvrir depuis N'IMPORTE QUEL ordinateur (Windows, Mac,
# Linux). Voir MODE_VITRINE.md.
# - /boot/animations : la même chose sur les systèmes plus anciens
# (Raspbian 11 « Bullseye » et avant).
# - le dossier « animations » à côté de ce programme.
ICI = os.path.dirname(os.path.abspath(__file__))
DOSSIERS_POSSIBLES = [
"/boot/firmware/animations",
"/boot/animations",
os.path.join(ICI, "animations"),
]
# Vitesse des SÉQUENCES d'images (sous-dossiers de PNG), en images par
# seconde, si le dossier ne contient pas de fichier « images_par_seconde.txt ».
# Les GIF, APNG et WebP, eux, contiennent leur propre vitesse : Krita
# l'enregistre à l'export.
IMAGES_PAR_SECONDE = 12
# Durée minimum d'une animation, en secondes. Une animation courte
# (un cœur qui bat en 1 s) est rejouée jusqu'à atteindre cette durée.
# Une image fixe reste affichée pendant cette durée.
DUREE_MINIMUM = 10
# Noir entre deux animations, en secondes (0 = enchaîner directement).
PAUSE_ENTRE_ANIMATIONS = 0.3
# HORAIRES de la vitrine : la matrice ne s'allume qu'entre ces deux heures.
# None = toujours allumée. Exemple : HORAIRES = ("09:00", "22:30")
HORAIRES = None
# Extensions de fichiers reconnues (en minuscules).
EXTENSIONS = (".gif", ".png", ".apng", ".webp", ".jpg", ".jpeg", ".bmp")
# =============================================================================
# 2. TROUVER LES ANIMATIONS
# =============================================================================
def trouver_dossier():
"""Renvoie le premier dossier d'animations qui existe."""
for dossier in DOSSIERS_POSSIBLES:
if os.path.isdir(dossier):
return dossier
return DOSSIERS_POSSIBLES[-1]
def est_cache(nom):
"""Les fichiers qui commencent par un point sont « cachés ».
Un Mac en crée sur les clés USB et cartes SD (._coeur.gif, .DS_Store) :
ce ne sont pas des images, il faut les ignorer."""
return nom.startswith(".")
def images_du_dossier(dossier):
"""Liste triée des images contenues dans un dossier (pour une séquence)."""
return sorted(
os.path.join(dossier, nom) for nom in os.listdir(dossier)
if not est_cache(nom) and nom.lower().endswith(EXTENSIONS)
)
def lister_animations(dossier):
"""Liste, par ordre alphabétique, ce qu'il y a à jouer dans le dossier :
les fichiers images, et les sous-dossiers qui contiennent des images."""
if not os.path.isdir(dossier):
return []
resultat = []
for nom in sorted(os.listdir(dossier), key=str.lower):
if est_cache(nom):
continue
chemin = os.path.join(dossier, nom)
if os.path.isdir(chemin):
if images_du_dossier(chemin):
resultat.append(chemin)
elif nom.lower().endswith(EXTENSIONS):
resultat.append(chemin)
return resultat
# =============================================================================
# 3. CHARGER UNE ANIMATION
# =============================================================================
#
# Une animation, pour nous, c'est une LISTE d'étapes. Chaque étape est un
# couple (image de 32 × 8, durée en secondes).
def adapter_image(image, nom):
"""Met n'importe quelle image au format de la matrice : 32 × 8, RVB."""
# 1) La transparence devient du noir (= LED éteinte).
image = image.convert("RGBA")
fond = Image.new("RGBA", image.size, (0, 0, 0, 255))
image = Image.alpha_composite(fond, image).convert("RGB")
largeur, hauteur = image.size
if (largeur, hauteur) == (LARGEUR, HAUTEUR):
return image # parfait, rien à faire
# 2) L'image est un AGRANDISSEMENT exact (320 × 80 = 32 × 8 fois 10) :
# on la réduit avec « le plus proche voisin », qui garde des pixels
# nets sans inventer de couleurs intermédiaires.
if largeur % LARGEUR == 0 and hauteur % HAUTEUR == 0 \
and largeur // LARGEUR == hauteur // HAUTEUR:
return image.resize((LARGEUR, HAUTEUR), PLUS_PROCHE_VOISIN)
# 3) Autre taille (par exemple 39 × 7, le format final) : on la pose
# au centre, sans la déformer. Ce qui dépasse est coupé.
avertir(nom, f"taille {largeur} × {hauteur} au lieu de {LARGEUR} × {HAUTEUR} : "
"image centrée (ce qui dépasse est coupé)")
toile = Image.new("RGB", (LARGEUR, HAUTEUR))
toile.paste(image, ((LARGEUR - largeur) // 2, (HAUTEUR - hauteur) // 2))
return toile
# Le nom du filtre « plus proche voisin » a changé avec Pillow 9.1 :
# on prend le nouveau s'il existe, sinon l'ancien (Raspbian 11 a Pillow 8).
PLUS_PROCHE_VOISIN = getattr(Image, "Resampling", Image).NEAREST
_deja_averti = set()
def avertir(nom, message):
"""Affiche un avertissement, une seule fois par fichier et par message."""
if (nom, message) not in _deja_averti:
_deja_averti.add((nom, message))
print(f" ⚠ {os.path.basename(nom)} : {message}")
def lire_vitesse(dossier):
"""Lit « images_par_seconde.txt » dans le dossier d'une séquence, s'il existe."""
fichier = os.path.join(dossier, "images_par_seconde.txt")
try:
with open(fichier) as f:
return float(f.read().strip().replace(",", "."))
except (OSError, ValueError):
return IMAGES_PAR_SECONDE
def charger_fichier(chemin):
"""Découpe un fichier image (fixe ou animé) en liste d'étapes."""
etapes = []
with Image.open(chemin) as fichier:
# ImageSequence.Iterator parcourt les images d'un fichier animé.
# Pour une image fixe, il n'y en a qu'une.
for image in ImageSequence.Iterator(fichier):
# La durée de chaque image est rangée dans le fichier, en ms.
# Certains GIF indiquent 0 : on prend alors 100 ms.
duree_ms = image.info.get("duration") or 100
etapes.append((adapter_image(image, chemin), duree_ms / 1000))
if len(etapes) == 1:
# Image fixe : on l'affiche pendant toute la durée minimum.
etapes = [(etapes[0][0], DUREE_MINIMUM)]
return etapes
def charger_sequence(dossier):
"""Charge un dossier de PNG numérotés comme une animation."""
duree = 1 / lire_vitesse(dossier)
etapes = []
for chemin in images_du_dossier(dossier):
with Image.open(chemin) as image:
etapes.append((adapter_image(image, dossier), duree))
return etapes
def charger(chemin):
"""Charge une animation, que ce soit un fichier ou un dossier."""
if os.path.isdir(chemin):
return charger_sequence(chemin)
return charger_fichier(chemin)
# =============================================================================
# 4. JOUER UNE ANIMATION
# =============================================================================
def jouer(etapes, duree_minimum=DUREE_MINIMUM):
"""Joue la liste d'étapes, et la rejoue jusqu'à atteindre la durée minimum."""
debut = time.monotonic()
while True:
for image, duree in etapes:
moment = time.monotonic()
matrice.montrer(image)
# On retire le temps déjà passé à afficher, pour garder le rythme.
matrice.attendre((duree - (time.monotonic() - moment)) * 1000)
if time.monotonic() - debut >= duree_minimum:
break
def ecran_d_attente(dossier):
"""Quand il n'y a rien à jouer : un petit point qui respire en bas à droite."""
print(f"Aucune animation dans {dossier} : en attente…")
for i in range(50):
matrice.effacer()
lumiere = int(60 + 60 * abs(25 - i) / 25)
matrice.pixel(LARGEUR - 1, HAUTEUR - 1, (0, lumiere, lumiere))
matrice.montrer()
matrice.attendre(100)
# =============================================================================
# 5. LES HORAIRES
# =============================================================================
def en_horaires():
"""Vrai si la vitrine doit être allumée à cette heure-ci."""
if HORAIRES is None:
return True
maintenant = datetime.datetime.now().strftime("%H:%M") # ex. "14:05"
debut, fin = HORAIRES
if debut <= fin:
return debut <= maintenant < fin # ex. de 09:00 à 22:30
return maintenant >= debut or maintenant < fin # ex. de 18:00 à 02:00 (passe minuit)
# =============================================================================
# 6. LE PROGRAMME PRINCIPAL
# =============================================================================
def main():
options = argparse.ArgumentParser(description="Joue les animations sur la matrice 8 × 32.")
options.add_argument("chemin", nargs="?",
help="un seul fichier ou dossier à jouer (facultatif)")
options.add_argument("--dossier", help="dossier d'animations à lire")
options.add_argument("--simulateur", action="store_true",
help="afficher dans une fenêtre au lieu de la matrice")
args = options.parse_args()
matrice.demarrer(simulateur=args.simulateur)
dossier = args.dossier or trouver_dossier()
try:
while True: # boucle infinie : la vitrine tourne jusqu'à ce qu'on l'arrête
if not en_horaires():
matrice.eteindre()
matrice.attendre(30_000) # on revérifie l'heure toutes les 30 s
continue
# Un seul fichier demandé, ou tout le dossier ?
playlist = [args.chemin] if args.chemin else lister_animations(dossier)
if not playlist:
ecran_d_attente(dossier)
continue
for chemin in playlist:
print("▶", os.path.relpath(chemin, dossier) if not args.chemin else chemin)
try:
etapes = charger(chemin)
except Exception as erreur:
# Un fichier abîmé ne doit pas arrêter la vitrine : on le saute.
print(f" ✗ impossible de lire ce fichier ({erreur}) : on passe au suivant")
continue
if etapes:
jouer(etapes)
if PAUSE_ENTRE_ANIMATIONS > 0:
matrice.effacer()
matrice.montrer()
matrice.attendre(PAUSE_ENTRE_ANIMATIONS * 1000)
if not en_horaires():
break
except KeyboardInterrupt:
# Ctrl + C dans le terminal : on sort proprement.
print("\nArrêt demandé.")
finally:
# « finally » s'exécute toujours, même en cas d'erreur :
# on ne laisse jamais la matrice allumée en partant.
try:
matrice.eteindre()
except Exception:
pass
# Cette ligne veut dire : « si on lance CE fichier directement, exécuter
# main() ». Quand un autre programme l'importe (outils/verifier_animation.py),
# main() n'est pas lancé : il peut juste réutiliser ses fonctions.
if __name__ == "__main__":
main()
+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()
+176
View File
@@ -0,0 +1,176 @@
"""
=============================================================================
MES ANIMATIONS — dessiner avec du code (en Python)
Workshop « Vitrine pixels »
=============================================================================
Krita, c'est pour DESSINER les animations image par image.
Ce fichier, c'est pour les CALCULER : hasard, mouvements continus,
arc-en-ciel… des choses très longues à dessiner à la main.
Ce sont les mêmes animations que dans matrice_8x32.ino (Arduino),
traduites en Python : comparez les deux fichiers, ligne à ligne !
Lancer :
python3 mes_animations.py --simulateur (sur l'ordinateur)
sudo ~/vitrine-env/bin/python mes_animations.py (sur le Pi)
Transformer la playlist en GIF, pour que le lecteur la joue avec les
dessins de l'équipe Krita (la playlist est alors jouée une seule fois) :
python3 mes_animations.py --enregistrer animations/10_code.gif
PYTHON EN 6 RÈGLES (pour qui connaît l'Arduino)
-----------------------------------------------
1. Pas de point-virgule en fin de ligne, pas d'accolades { }.
2. Ce sont les ESPACES en début de ligne (l'indentation) qui disent ce
qui est « dans » une boucle ou une fonction. 4 espaces par niveau.
3. Pas de type à déclarer : x = 3 au lieu de int x = 3;
4. Une fonction : def nom(): au lieu de void nom() {
5. Une boucle : for x in range(32): (x va de 0 à 31)
au lieu de for (int x = 0; x < 32; x++) {
6. Un commentaire commence par # au lieu de //
LES OUTILS DE DESSIN (définis dans matrice.py)
----------------------------------------------
pixel(x, y, couleur) colore un pixel (x de 0 à 31, y de 0 à 7)
lire_pixel(x, y) renvoie la couleur d'un pixel
effacer() éteint tout (dans la toile)
effacer(couleur) remplit tout d'une couleur
assombrir(25) assombrit tout de 25 % (traînées)
montrer() AFFICHE la toile (sinon rien n'apparaît !)
attendre(100) attend 100 millisecondes
Couleurs : ROUGE, ORANGE, JAUNE, VERT, CYAN, BLEU, VIOLET, MAGENTA, ROSE,
BLANC, NOIR, ou (rouge, vert, bleu) de 0 à 255,
ou teinte(t) avec t de 0 à 255 pour faire le tour de l'arc-en-ciel.
"""
import random # pour le hasard
import sys # pour lire les options de la ligne de commande
# « from matrice import * » = importer TOUS les outils du moteur, pour
# écrire pixel(...) au lieu de matrice.pixel(...).
from matrice import *
# =============================================================================
# LES ANIMATIONS
# =============================================================================
# À VOUS DE JOUER : copiez une animation, renommez-la, modifiez-la,
# et ajoutez son nom dans la PLAYLIST tout en bas.
# --- Animation 1 : arc-en-ciel qui défile ---
def anim_arc_en_ciel():
for temps in range(256):
for x in range(LARGEUR):
for y in range(HAUTEUR):
# La teinte dépend de la colonne et du temps qui passe.
pixel(x, y, teinte(x * 8 + temps * 2))
montrer()
attendre(20)
# --- Animation 2 : la pluie ---
def anim_pluie():
for image in range(200):
assombrir(25) # les gouttes laissent une traînée qui s'estompe
# Faire descendre toute l'image d'une ligne, en partant du bas
# (range(7, 0, -1) compte à rebours : 7, 6, 5… 1).
for y in range(HAUTEUR - 1, 0, -1):
for x in range(LARGEUR):
pixel(x, y, lire_pixel(x, y - 1))
# La ligne du haut s'éteint, puis 3 nouvelles gouttes au hasard.
for x in range(LARGEUR):
pixel(x, 0, NOIR)
for n in range(3):
pixel(random.randrange(LARGEUR), 0, (0, 120, 255))
montrer()
attendre(60)
# --- Animation 3 : un dessin en pixel art qui défile ---
# Une lettre par pixel, « . » = éteint. 8 lignes de même longueur.
DESSIN = [
"..RR.RR..",
".RBRRRRR.",
".RRRRRRR.",
".RRRRRRR.",
"..RRRRR..",
"...RRR...",
"....R....",
".........",
]
# Un « dictionnaire » associe chaque lettre à une couleur.
COULEUR_DE_LETTRE = {
"R": ROUGE, "O": ORANGE, "J": JAUNE, "V": VERT, "C": CYAN,
"B": BLANC, "L": BLEU, "M": MAGENTA, "P": ROSE,
}
def dessiner_dessin(depart_x):
for y, ligne in enumerate(DESSIN): # enumerate donne le numéro ET la ligne
for x, lettre in enumerate(ligne):
if lettre in COULEUR_DE_LETTRE:
pixel(depart_x + x, y, COULEUR_DE_LETTRE[lettre])
def anim_dessin_qui_defile():
largeur_dessin = len(DESSIN[0]) # len = longueur de la ligne
for x in range(LARGEUR, -largeur_dessin - 1, -1):
effacer()
dessiner_dessin(x)
montrer()
attendre(70)
# --- Animation 4 : étincelles ---
def anim_etincelles():
for image in range(300):
assombrir(8)
pixel(random.randrange(LARGEUR), random.randrange(HAUTEUR),
teinte(random.randrange(256), saturation=200))
montrer()
attendre(15)
# --- Exemple : votre première animation ---
def anim_ma_premiere():
for x in range(LARGEUR):
effacer()
pixel(x, 3, JAUNE)
pixel(x, 4, JAUNE)
montrer()
attendre(50)
# =============================================================================
# LA PLAYLIST
# =============================================================================
# Mettez # devant une ligne pour sauter une animation.
def playlist():
anim_arc_en_ciel()
anim_pluie()
anim_dessin_qui_defile()
anim_etincelles()
# anim_ma_premiere()
if __name__ == "__main__":
demarrer()
try:
while True: # en boucle, comme loop() sur l'Arduino
playlist()
if "--enregistrer" in sys.argv:
break # en enregistrement, un seul tour suffit
except KeyboardInterrupt:
print("\nArrêt demandé.")
finally:
eteindre()
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""
=============================================================================
GÉNÉRER LES ANIMATIONS D'EXEMPLE (et la palette Krita)
Workshop « Vitrine pixels »
=============================================================================
Fabrique, dans le dossier « animations », un exemple de chaque format que
le lecteur sait jouer :
01_coeur_qui_bat.gif un GIF animé (comme un export Krita en GIF)
02_texte_defilant.gif un texte qui défile, écrit avec une police 5 × 7
03_vague/ une SÉQUENCE de PNG numérotés + images_par_seconde.txt
(comme un export Krita en « séquence d'images »)
04_balle.png un PNG animé (APNG)
Et la palette Krita « krita/palette_vitrine_pixels.gpl ».
Usage :
python3 outils/generer_exemples.py
C'est aussi un exemple : on peut FABRIQUER des images par le code avec
Pillow, puis les jouer comme des dessins.
"""
import math
import os
from PIL import Image
ICI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ANIMATIONS = os.path.join(ICI, "animations")
L, H = 32, 8
def nouvelle_image():
return Image.new("RGB", (L, H))
def dessiner(image, motif, x0, y0, couleurs):
"""Dessine un motif texte (une lettre = une couleur, « . » = rien)."""
for y, ligne in enumerate(motif):
for x, lettre in enumerate(ligne):
if lettre in couleurs and 0 <= x0 + x < L and 0 <= y0 + y < H:
image.putpixel((x0 + x, y0 + y), couleurs[lettre])
# --- 01 : cœur qui bat (GIF) -------------------------------------------------
PETIT_COEUR = [
".RR.RR.",
"RBRRRRR",
"RRRRRRR",
".RRRRR.",
"..RRR..",
"...R...",
]
GRAND_COEUR = [
".RRR.RRR.",
"RBBRRRRRR",
"RBRRRRRRR",
"RRRRRRRRR",
".RRRRRRR.",
"..RRRRR..",
"...RRR...",
"....R....",
]
def coeur_qui_bat():
couleurs = {"R": (230, 0, 40), "B": (255, 170, 190)}
images, durees = [], []
# petit → grand → petit → pause : le rythme d'un battement « boum-boum »
for motif, duree in [(PETIT_COEUR, 400), (GRAND_COEUR, 120), (PETIT_COEUR, 120),
(GRAND_COEUR, 160)]:
im = nouvelle_image()
largeur = len(motif[0])
dessiner(im, motif, (L - largeur) // 2, (H - len(motif)) // 2 + (len(motif) == 6), couleurs)
images.append(im)
durees.append(duree)
images[0].save(os.path.join(ANIMATIONS, "01_coeur_qui_bat.gif"), save_all=True,
append_images=images[1:], duration=durees, loop=0)
# --- 02 : texte défilant (GIF) -----------------------------------------------
# Police 5 × 7 : chaque lettre est dessinée avec des « # ».
POLICE = {
"A": [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
"D": ["####.", "#...#", "#...#", "#...#", "#...#", "#...#", "####."],
"E": ["#####", "#....", "#....", "####.", "#....", "#....", "#####"],
"H": ["#...#", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
"I": ["###", ".#.", ".#.", ".#.", ".#.", ".#.", "###"],
"L": ["#....", "#....", "#....", "#....", "#....", "#....", "#####"],
"N": ["#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "#...#"],
"P": ["####.", "#...#", "#...#", "####.", "#....", "#....", "#...."],
"R": ["####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#"],
"S": [".####", "#....", "#....", ".###.", "....#", "....#", "####."],
"T": ["#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."],
"V": ["#...#", "#...#", "#...#", "#...#", "#...#", ".#.#.", "..#.."],
"X": ["#...#", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "#...#"],
" ": ["...", "...", "...", "...", "...", "...", "..."],
}
def texte_defilant(texte="VITRINE PIXELS"):
# 1) On écrit tout le texte sur une longue bande.
largeur_totale = sum(len(POLICE[c][0]) + 1 for c in texte)
bande = Image.new("RGB", (largeur_totale, H))
x = 0
for i, c in enumerate(texte):
# chaque lettre a sa propre teinte : un petit arc-en-ciel
r, v, b = [int(255 * (0.5 + 0.5 * math.cos(2 * math.pi * (i / len(texte) + k / 3))))
for k in range(3)]
for y, ligne in enumerate(POLICE[c]):
for dx, p in enumerate(ligne):
if p == "#":
bande.putpixel((x + dx, y), (r, v, b))
x += len(POLICE[c][0]) + 1
# 2) On fait glisser une « fenêtre » de 32 pixels sur la bande.
images = []
for decalage in range(-L, largeur_totale + 1):
im = nouvelle_image()
im.paste(bande, (-decalage, 0))
images.append(im)
images[0].save(os.path.join(ANIMATIONS, "02_texte_defilant.gif"), save_all=True,
append_images=images[1:], duration=60, loop=0)
# --- 03 : vague (séquence de PNG) --------------------------------------------
def vague(nb_images=40, ips=20):
dossier = os.path.join(ANIMATIONS, "03_vague")
os.makedirs(dossier, exist_ok=True)
for n in range(nb_images):
im = nouvelle_image()
phase = 2 * math.pi * n / nb_images
for x in range(L):
hauteur = (H - 1) / 2 * (1 + math.sin(2 * math.pi * x / L + phase))
for y in range(H):
if H - 1 - y <= hauteur: # sous la crête de la vague
# 3 bleus seulement, du plus clair (crête) au plus profond :
# un rendu « pixel art » franc, sans dégradé flou.
profondeur = min(2, int(hauteur - (H - 1 - y)))
im.putpixel((x, y), [(120, 220, 255), (0, 120, 255), (0, 30, 160)][profondeur])
# Même nommage que Krita : nom_0000.png, nom_0001.png…
im.save(os.path.join(dossier, f"vague_{n:04d}.png"))
with open(os.path.join(dossier, "images_par_seconde.txt"), "w") as f:
f.write(f"{ips}\n")
# --- 04 : balle qui rebondit (APNG) ------------------------------------------
def balle(nb_images=48):
images = []
for n in range(nb_images):
im = nouvelle_image()
t = n / nb_images
x = round((L - 2) * (1 - abs(2 * t - 1))) # aller-retour
y = round((H - 2) * (1 - abs(math.sin(4 * math.pi * t)))) # rebonds
for dx in (0, 1):
for dy in (0, 1):
im.putpixel((x + dx, y + dy), (255, 150, 0))
images.append(im)
images[0].save(os.path.join(ANIMATIONS, "04_balle.png"), save_all=True,
append_images=images[1:], duration=40, loop=0)
# --- Palette Krita -----------------------------------------------------------
def palette_krita():
"""Palette au format GIMP (.gpl), que Krita sait importer.
Des couleurs SATURÉES, qui rendent bien sur des LED, en 3 intensités,
plus les pastels (mélange avec du blanc) et le noir/blanc."""
teintes = [("Rouge", (255, 0, 0)), ("Orange", (255, 90, 0)), ("Jaune", (255, 200, 0)),
("Vert", (0, 255, 0)), ("Cyan", (0, 255, 200)), ("Bleu", (0, 60, 255)),
("Violet", (140, 0, 255)), ("Rose", (255, 0, 110))]
lignes = ["GIMP Palette", "Name: Vitrine pixels (LED)", "Columns: 8", "#"]
def ajouter(c, nom):
lignes.append(f"{c[0]:3d} {c[1]:3d} {c[2]:3d}\t{nom}")
for nom, c in teintes:
ajouter(c, nom)
for nom, c in teintes: # 60 % : valeur minimum conseillée pour les « moyens »
ajouter(tuple(round(k * 0.6) for k in c), nom + " moyen")
for nom, c in teintes: # 35 % : la plus sombre qui reste bien visible
ajouter(tuple(round(k * 0.35) for k in c), nom + " sombre")
for nom, c in teintes:
ajouter(tuple(round(k + (255 - k) * 0.55) for k in c), nom + " pastel")
for nom, c in [("Noir (éteint)", (0, 0, 0)), ("Gris sombre", (90, 90, 90)),
("Gris", (160, 160, 160)), ("Blanc chaud", (255, 200, 140)),
("Blanc", (255, 255, 255))]:
ajouter(c, nom)
os.makedirs(os.path.join(ICI, "krita"), exist_ok=True)
with open(os.path.join(ICI, "krita", "palette_vitrine_pixels.gpl"), "w") as f:
f.write("\n".join(lignes) + "\n")
if __name__ == "__main__":
os.makedirs(ANIMATIONS, exist_ok=True)
coeur_qui_bat()
texte_defilant()
vague()
balle()
palette_krita()
print("Exemples créés dans", ANIMATIONS)
print("Palette créée dans", os.path.join(ICI, "krita"))
+316
View File
@@ -0,0 +1,316 @@
#!/usr/bin/env python3
"""Dessine les schémas du dossier « schemas » :
- branchement.svg : Raspberry Pi + 74AHCT125 + matrice + alimentation
- brochage_74ahct125.svg : la puce adaptatrice de niveau, patte par patte
- chaine_krita_matrice.svg : le trajet d'une animation, de Krita à la matrice
Usage : python3 outils/schemas.py"""
import os
ICI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SORTIE = os.path.join(ICI, "schemas")
ROUGE, NOIR, BLANC_FIL, VERT = "#d62828", "#1a1a1a", "#bdbdbd", "#2a9d3a"
JAUNE_GPIO = "#e0a800"
class Svg:
def __init__(self, l, h, titre):
self.l, self.h = l, h
self.out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{l}" height="{h}" '
f'viewBox="0 0 {l} {h}" font-family="Helvetica, Arial, sans-serif">',
f' <title>{titre}</title>',
' <defs><marker id="fleche" viewBox="0 0 10 10" refX="8" refY="5" '
'markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" '
'fill="context-stroke"/></marker>'
'<marker id="flecheg" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" '
'markerHeight="6" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#555"/></marker>'
'<marker id="flechec" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" '
'markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#cfcfcf"/></marker>'
'</defs>',
f' <rect width="{l}" height="{h}" fill="#ffffff"/>']
def add(self, s):
self.out.append(" " + s)
def rect(self, x, y, w, h, fill, rx=0, stroke=None, sw=1):
st = f' stroke="{stroke}" stroke-width="{sw}"' if stroke else ""
self.add(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{fill}"{st}/>')
def texte(self, x, y, s, taille=13, c="#333", ancre="start", gras=False, mono=False):
g = ' font-weight="bold"' if gras else ""
m = ' font-family="monospace"' if mono else ""
self.add(f'<text x="{x}" y="{y}" font-size="{taille}" fill="{c}" '
f'text-anchor="{ancre}"{g}{m}>{s}</text>')
def fil(self, d, c, w=5):
self.add(f'<path d="{d}" stroke="{c}" stroke-width="{w}" fill="none" '
'stroke-linecap="round" stroke-linejoin="round"/>')
def point(self, x, y, c, r=5):
self.add(f'<circle cx="{x}" cy="{y}" r="{r}" fill="{c}"/>')
def save(self, nom):
self.out.append("</svg>")
with open(os.path.join(SORTIE, nom), "w") as f:
f.write("\n".join(self.out) + "\n")
def hop_h(x_croise, sens=1, r=7):
"""Petit pont (demi-cercle) pour un fil horizontal qui en enjambe un autre.
sens = 1 : on va vers la droite ; -1 : vers la gauche."""
return (f"H{x_croise - sens * r} a{r} {r} 0 0 {1 if sens == 1 else 0} "
f"{2 * r * sens} 0")
# =============================================================================
# 1. BRANCHEMENT
# =============================================================================
def branchement():
s = Svg(1200, 940, "Branchement matrice WS2812B 8×32, Raspberry Pi, 74AHCT125 "
"et alimentation d'établi")
# ---------- matrice (vue de dos) ----------
s.texte(60, 38, "Matrice WS2812B 8 × 32 — vue de dos (côté fils)", 20, "#222", gras=True)
s.add('<pattern id="pix" x="60" y="55" width="30.625" height="30.625" '
'patternUnits="userSpaceOnUse"><rect x="9" y="9" width="12" height="12" rx="1" '
'fill="#2b2b2b"/></pattern>')
s.rect(60, 55, 980, 245, "#141414", 6)
s.rect(60, 55, 980, 245, "url(#pix)", 6)
s.add('<line x1="760" y1="150" x2="340" y2="150" stroke="#cfcfcf" stroke-width="3" '
'marker-end="url(#flechec)"/>')
s.texte(550, 135, "les données entrent par DIN et traversent les 256 LED", 15, "#e6e6e6", "middle")
for x, t in [(110, "5V"), (150, "GND"), (194, "DOUT"), (490, "5V"), (550, "GND"),
(880, "5V"), (910, "GND"), (940, "DIN")]:
s.texte(x, 268, t, 12, "#cfcfcf", "middle")
# ---------- sortie DOUT ----------
for x, c in [(110, ROUGE), (150, BLANC_FIL), (190, VERT)]:
s.fil(f"M{x} 282 V340", c)
s.rect(96, 340, 108, 24, "#222", 3)
s.texte(150, 390, "Sortie DOUT : ne rien brancher", 14, "#555", "middle")
s.texte(150, 408, "(sert à chaîner un 2e panneau)", 13, "#888", "middle")
# ---------- alimentation de la matrice (fils du milieu) ----------
s.fil("M490 282 V440 H530 V586", ROUGE, 6)
s.fil("M550 282 V420 H590 V586", NOIR, 6)
s.add('<line x1="490" y1="352" x2="506" y2="352" stroke="#777" stroke-width="2"/>')
s.add('<line x1="534" y1="352" x2="550" y2="352" stroke="#777" stroke-width="2"/>')
s.rect(506, 340, 28, 24, "#1f4e9c", 5)
s.rect(524, 340, 10, 24, "#c9d6ee")
s.texte(529, 356, "−", 13, "#1f4e9c", "middle", True)
s.texte(474, 340, "Condensateur 1000 µF", 14, "#1f4e9c", "end", True)
s.texte(474, 358, "bande « − » côté GND (fil noir)", 13, "#1f4e9c", "end")
s.texte(474, 375, "patte + côté 5V (fil rouge)", 13, "#1f4e9c", "end")
# ---------- alimentation d'établi ----------
s.rect(300, 500, 330, 210, "#e9ecef", 10, "#adb5bd", 2)
s.texte(465, 526, "Alimentation d'établi", 16, "#333", "middle", True)
s.rect(318, 540, 160, 96, "#1d2b1d", 4)
s.texte(398, 578, "5.00 V", 28, "#7dff7d", "middle", True, True)
s.texte(398, 618, "3.00 A", 20, "#ffb347", "middle", mono=True)
for x, t in [(345, "tension"), (420, "courant")]:
s.add(f'<circle cx="{x}" cy="672" r="16" fill="#666"/>')
s.texte(x, 702, t, 11, "#555", "middle")
s.add(f'<circle cx="530" cy="600" r="13" fill="{ROUGE}" stroke="#7a0f0f" stroke-width="2"/>')
s.add(f'<circle cx="590" cy="600" r="13" fill="{NOIR}" stroke="#000" stroke-width="2"/>')
s.texte(530, 637, "+", 15, ROUGE, "middle", True)
s.texte(590, 637, "−", 15, NOIR, "middle", True)
s.texte(560, 660, "bornes de", 12, "#555", "middle")
s.texte(560, 675, "sortie", 12, "#555", "middle")
# ---------- entrée DIN + fiche ----------
for x, c in [(880, ROUGE), (910, BLANC_FIL), (940, VERT)]:
s.fil(f"M{x} 282 V340", c)
s.rect(866, 340, 88, 22, "#222", 3)
s.rect(870, 364, 80, 22, "#3a3a3a", 3)
s.texte(966, 358, "connecteur DIN de la matrice", 13, "#555")
s.texte(966, 381, "+ fiche fournie (rouge / blanc / vert)", 13, "#555")
# ---------- plaque d'essai + 74AHCT125 ----------
s.rect(700, 425, 440, 190, "#f6f1e3", 8, "#d9cfb2", 2)
s.texte(1132, 608, "plaque d'essai", 12, "#a8997a", "end")
def broche_bas(n): # pattes 1 à 7 (rangée du bas), de gauche à droite
return 815 + (n - 1) * 28
def broche_haut(n): # pattes 14 à 8 (rangée du haut), de gauche à droite
return 815 + (14 - n) * 28
# fils de la fiche vers la plaque
s.fil(f"M880 386 V444 H{broche_haut(14)} V462", ROUGE) # 5V → VCC (14)
s.fil(f"M910 386 V454 {hop_h(broche_haut(14), -1)} H780 V585", BLANC_FIL) # GND → rail
s.fil(f"M940 386 V444 H1080 V470", VERT) # DIN → résistance
s.fil(f"M1080 510 V552 {hop_h(broche_bas(7), -1)} H{broche_bas(3)} V528", VERT)
# rail de masse
s.fil(f"M780 585 {hop_h(broche_bas(2), 1)} H{broche_bas(7)}", NOIR, 4)
s.fil(f"M{broche_bas(1)} 528 V585", NOIR, 4) # 1OE → GND
s.fil(f"M{broche_bas(7)} 528 V585", NOIR, 4) # GND (7)
for x in (780, broche_bas(1), broche_bas(7)):
s.point(x, 585, NOIR)
# la puce
s.rect(805, 470, 192, 50, "#222", 4)
s.add('<path d="M805 485 a10 10 0 0 1 0 20" fill="#555"/>')
s.texte(901, 500, "74AHCT125", 15, "#eee", "middle", True)
for n in range(1, 8):
s.rect(broche_bas(n) - 5, 520, 10, 8, "#b8b8b8")
s.texte(broche_bas(n), 515, str(n), 9, "#bbb", "middle")
for n in range(8, 15):
s.rect(broche_haut(n) - 5, 462, 10, 8, "#b8b8b8")
s.texte(broche_haut(n), 482, str(n), 9, "#bbb", "middle")
s.texte(broche_bas(1) - 10, 545, "1OE", 11, "#333", "end", True)
s.texte(broche_bas(2) + 6, 545, "1A", 11, JAUNE_GPIO, "start", True)
s.texte(broche_bas(3) + 6, 545, "1Y", 11, VERT, "start", True)
s.texte(broche_bas(7) + 8, 545, "GND", 11, "#333", "start", True)
# résistance 330 Ω
s.rect(1071, 470, 18, 40, "#e8d3a8", 6, "#a88a52")
for y, c in [(478, "#e67e22"), (486, "#e67e22"), (494, "#8b4513")]:
s.rect(1071, y, 18, 4, c)
s.rect(1071, 502, 18, 3, "#c9a227")
s.texte(1098, 486, "330 Ω", 13, "#7a5a1e", gras=True)
s.texte(966, 404, "fil rouge de la fiche : il alimente", 12, ROUGE, gras=True)
s.texte(966, 418, "la puce (patte 14 = VCC, 5 V)", 12, ROUGE, gras=True)
# ---------- Raspberry Pi ----------
s.rect(700, 645, 440, 230, "#1f7a3a", 12)
s.rect(750, 655, 336, 42, "#143d20", 3)
x0 = broche_bas(2) - 80 # x de la patte 2 → la patte 12 tombe sous « 1A »
for i in range(20):
for rang, y in ((0, 666), (1, 684)): # rangée du bord : pattes paires
n = 2 * i + 2 if rang == 0 else 2 * i + 1
couleur = "#d4af37"
if n in (2, 4):
couleur = ROUGE
elif n in (6, 14):
couleur = NOIR
elif n == 12:
couleur = JAUNE_GPIO
s.rect(x0 + i * 16 - 5, y - 5, 10, 10, couleur, 1)
s.texte(x0 - 9, 670, "2", 9, "#cde", "end")
s.texte(x0 - 9, 688, "1", 9, "#cde", "end")
s.texte(x0 + 19 * 16 + 9, 670, "40", 9, "#cde")
s.texte(x0 + 19 * 16 + 9, 688, "39", 9, "#cde")
# GPIO 18 (patte 12) → 1A (patte 2)
s.fil(f"M{broche_bas(2)} 528 V661", JAUNE_GPIO)
# GND (patte 6) ← rail
s.fil(f"M780 585 V630 H{x0 + 32} V661", NOIR, 4)
s.texte(920, 770, "Raspberry Pi", 24, "#fff", "middle", True)
s.texte(920, 794, "3B+, 4 ou Zero 2 W (pas le Pi 5)", 14, "#d8f0dd", "middle")
s.texte(x0 + 80, 712, "patte 12 = GPIO 18", 12, "#ffe08a", "start", True)
s.texte(x0 + 30, 712, "patte 6 = GND", 12, "#fff", "end", True)
s.add(f'<path d="M{x0 + 8} 833 L{x0 + 24} 849 M{x0 + 24} 833 L{x0 + 8} 849" '
f'stroke="#ffb3b3" stroke-width="3"/>')
s.texte(x0 + 34, 846, "pattes 2 et 4 (5 V) : NE PAS relier à la matrice", 12, "#ffd6d6")
# alimentation USB du Pi
s.rect(1140, 790, 18, 34, "#b8b8b8", 3)
s.fil("M1158 807 H1180 V900", "#555")
s.texte(1170, 918, "bloc secteur USB du Pi", 13, "#333", "end")
s.texte(1170, 935, "(5 V, 2,5 A ou plus)", 12, "#777", "end")
# ---------- légende ----------
s.rect(40, 470, 236, 300, "#f8f9fa", 8, "#dee2e6")
s.texte(56, 496, "Code couleur des fils", 15, "#222", gras=True)
for i, (c, t) in enumerate([(ROUGE, "rouge = + 5 V"), (NOIR, "noir = GND (masse)"),
(BLANC_FIL, "blanc = GND (masse)"), (VERT, "vert = données 5 V"),
(JAUNE_GPIO, "jaune = données 3,3 V")]):
y = 520 + i * 28
s.add(f'<line x1="56" y1="{y}" x2="90" y2="{y}" stroke="{c}" stroke-width="6" '
'stroke-linecap="round"/>')
s.texte(100, y + 5, t, 14)
s.texte(56, 670, "Tous les GND sont reliés :", 13, "#555")
s.texte(56, 688, "Pi ↔ puce ↔ fil blanc ↔ matrice", 13, "#555")
s.texte(56, 706, "↔ fil noir ↔ borne − de l'alim.", 13, "#555")
s.texte(56, 730, "Alim. réglée sur 5,0 V maxi !", 13, "#b00020", gras=True)
s.texte(56, 752, "Un pont = les fils se croisent", 12, "#777")
s.texte(56, 766, "sans se toucher.", 12, "#777")
s.save("branchement.svg")
# =============================================================================
# 2. BROCHAGE DU 74AHCT125
# =============================================================================
def brochage():
s = Svg(820, 470, "Brochage du 74AHCT125 (vu de dessus)")
s.texte(410, 36, "74AHCT125 vu de DESSUS (encoche à gauche)", 20, "#222", "middle", True)
s.rect(250, 150, 320, 110, "#222", 6)
s.add('<path d="M250 185 a20 20 0 0 1 0 40" fill="#555"/>')
s.add('<circle cx="275" cy="240" r="6" fill="#555"/>')
s.texte(410, 212, "74AHCT125", 24, "#eee", "middle", True)
bas = {1: ("1OE", "→ GND", "#333"), 2: ("1A", "← GPIO 18", JAUNE_GPIO),
3: ("1Y", "→ 330 Ω → DIN", VERT), 4: ("2OE", "", "#aaa"), 5: ("2A", "", "#aaa"),
6: ("2Y", "", "#aaa"), 7: ("GND", "→ GND", "#333")}
haut = {14: ("VCC", "← 5 V", ROUGE), 13: ("4OE", "", "#aaa"), 12: ("4A", "", "#aaa"),
11: ("4Y", "", "#aaa"), 10: ("3OE", "", "#aaa"), 9: ("3A", "", "#aaa"),
8: ("3Y", "", "#aaa")}
for n, (nom, role, c) in bas.items():
x = 280 + (n - 1) * 43
s.rect(x - 8, 260, 16, 26, "#b8b8b8")
s.texte(x, 252, str(n), 12, "#bbb", "middle")
s.texte(x, 306, nom, 14, c, "middle", True)
if role:
y = 338 + {1: 0, 2: 22, 3: 44, 7: 0}[n] * 1
s.add(f'<line x1="{x}" y1="312" x2="{x}" y2="{y - 13}" stroke="{c}" stroke-width="1.5"/>')
s.texte(x, y, role, 13, c, "middle", True)
for n, (nom, role, c) in haut.items():
x = 280 + (14 - n) * 43
s.rect(x - 8, 124, 16, 26, "#b8b8b8")
s.texte(x, 168, str(n), 12, "#bbb", "middle")
s.texte(x, 114, nom, 14, c, "middle", True)
if role:
s.texte(x, 92, role, 13, c, "middle", True)
s.texte(40, 445, "On n'utilise qu'une des 4 « portes » (la n° 1). Pattes en gris : non branchées "
"(on peut relier 4, 10 et 13 au 5 V pour les désactiver).", 13, "#555")
s.save("brochage_74ahct125.svg")
# =============================================================================
# 3. DE KRITA À LA MATRICE
# =============================================================================
def chaine():
s = Svg(1200, 330, "De Krita à la matrice : le trajet d'une animation")
etapes = [
("#7b3fa0", "1. Dessiner", ["Krita", "document 32 × 8 px", "pixel art, sans flou"]),
("#b5418f", "2. Exporter", ["GIF animé", "ou séquence de PNG", "(Rendu de l'animation)"]),
("#c9682a", "3. Vérifier", ["verifier_animation.py", "+ simulateur", "sur l'ordinateur"]),
("#b08900", "4. Déposer", ["dossier « animations »", "carte SD ou réseau", "(01_, 02_… = ordre)"]),
("#1f7a3a", "5. Lire", ["Raspberry Pi", "lecteur.py : gamma,", "luminosité, serpentin"]),
("#2f6fc4", "6. Afficher", ["74AHCT125 (3,3 → 5 V)", "puis 256 LED", "WS2812B"]),
]
larg, ecart = 170, 28
for i, (c, titre, lignes) in enumerate(etapes):
x = 30 + i * (larg + ecart)
s.rect(x, 60, larg, 190, "#ffffff", 12, c, 3)
s.rect(x, 60, larg, 46, c, 12)
s.rect(x, 90, larg, 16, c)
s.texte(x + larg / 2, 90, titre, 18, "#fff", "middle", True)
for j, t in enumerate(lignes):
s.texte(x + larg / 2, 140 + j * 26, t, 14 if j == 0 else 13,
"#222" if j == 0 else "#555", "middle", j == 0)
if i < len(etapes) - 1:
s.add(f'<line x1="{x + larg + 4}" y1="155" x2="{x + larg + ecart - 4}" y2="155" '
'stroke="#555" stroke-width="3" marker-end="url(#flecheg)"/>')
s.texte(30, 36, "De Krita à la matrice", 22, "#222", gras=True)
s.add('<path d="M115 270 V292 H1003 V270" stroke="#aaa" stroke-width="2" fill="none" '
'stroke-dasharray="6 5"/>')
s.texte(559, 316, "Étapes 1 à 4 : l'équipe design, sur n'importe quel ordinateur. "
"Étapes 5 et 6 : automatiques, dans la vitrine.", 14, "#555", "middle")
s.save("chaine_krita_matrice.svg")
if __name__ == "__main__":
os.makedirs(SORTIE, exist_ok=True)
branchement()
brochage()
chaine()
print("Schémas écrits dans", SORTIE)
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""
=============================================================================
VÉRIFIER UNE ANIMATION avant de l'envoyer sur la matrice
Workshop « Vitrine pixels »
=============================================================================
Pour l'équipe design : ce programme examine un export de Krita et dit
s'il est prêt pour la matrice. Il fonctionne sur n'importe quel ordinateur
(il suffit de Python et de Pillow), sans la matrice.
Usage :
python3 outils/verifier_animation.py mon_animation.gif
python3 outils/verifier_animation.py dossier_de_png/
python3 outils/verifier_animation.py animations/ (tout le dossier)
Il affiche un petit rapport :
- taille de l'image, nombre d'images, durée, vitesse ;
- nombre de couleurs (beaucoup de couleurs = souvent de l'anticrénelage,
ces pixels « flous » qui adoucissent les bords dans Krita) ;
- pixels trop sombres, qui seront ÉTEINTS sur la matrice ;
- courant consommé, pour vérifier que l'alimentation suit.
Et il fabrique, dans le dossier « apercus », deux APERÇUS « façon LED » :
- mon_animation_apercu.png : planche avec toutes les images ;
- mon_animation_apercu.gif : l'animation agrandie, à partager.
(Pas dans le dossier des animations : le lecteur les jouerait aussi !)
"""
import os
import sys
from PIL import Image, ImageDraw
# On réutilise le code du lecteur et du moteur (dossier parent).
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import lecteur # noqa: E402
from matrice import LARGEUR, HAUTEUR, TABLE_GAMMA, LUMINOSITE, courant_estime_ma, COURANT_MAX_MA, pixels_de # noqa: E402
DOSSIER_APERCUS = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "apercus")
TAILLE_LED = 14 # taille d'un pixel dans l'aperçu, en points
MAX_PLANCHE = 48 # nombre maximum d'images sur la planche
def pixel_eteint_sur_matrice(r, v, b):
"""Vrai si ce pixel, allumé dans l'image, sera éteint sur la matrice
(une fois la correction gamma et la luminosité appliquées)."""
if (r, v, b) == (0, 0, 0):
return False
return all(TABLE_GAMMA[c] * LUMINOSITE // 255 == 0 for c in (r, v, b))
def dessiner_leds(image, taille=TAILLE_LED):
"""Agrandit une image 32 × 8 en dessinant un rond par pixel."""
sortie = Image.new("RGB", (LARGEUR * taille, HAUTEUR * taille), (12, 12, 12))
crayon = ImageDraw.Draw(sortie)
for y in range(HAUTEUR):
for x in range(LARGEUR):
couleur = image.getpixel((x, y))
if couleur == (0, 0, 0):
couleur = (30, 30, 30) # LED éteinte : gris très sombre
marge = max(1, taille // 7)
crayon.ellipse((x * taille + marge, y * taille + marge,
(x + 1) * taille - marge, (y + 1) * taille - marge), fill=couleur)
return sortie
def verifier(chemin):
nom = os.path.basename(os.path.normpath(chemin))
print("=" * 70)
print(nom)
print("=" * 70)
# Taille d'origine (avant adaptation par le lecteur).
if os.path.isdir(chemin):
fichiers = lecteur.images_du_dossier(chemin)
if not fichiers:
print(" ✗ aucun fichier image dans ce dossier")
return
with Image.open(fichiers[0]) as premiere:
taille = premiere.size
print(f" Type : séquence de {len(fichiers)} fichiers "
f"({lecteur.lire_vitesse(chemin):g} images/s)")
else:
with Image.open(chemin) as fichier:
taille = fichier.size
print(f" Type : {fichier.format}, "
f"{'animé' if getattr(fichier, 'is_animated', False) else 'image fixe'}")
etapes = lecteur.charger(chemin)
duree = sum(d for _, d in etapes)
# --- Taille ---
if taille == (LARGEUR, HAUTEUR):
print(f" Taille : {taille[0]} × {taille[1]} ✓")
elif taille[0] % LARGEUR == 0 and taille[0] // LARGEUR == taille[1] // HAUTEUR \
and taille[1] % HAUTEUR == 0:
print(f" Taille : {taille[0]} × {taille[1]} ✓ (agrandissement × "
f"{taille[0] // LARGEUR}, sera réduit)")
else:
print(f" Taille : {taille[0]} × {taille[1]} ⚠ attendu {LARGEUR} × {HAUTEUR} "
"(l'image sera centrée et coupée)")
# --- Durée ---
if len(etapes) > 1:
print(f" Images : {len(etapes)}, durée {duree:.2f} s "
f"(≈ {len(etapes) / duree:.1f} images/s)")
else:
print(" Images : 1 (image fixe)")
# --- Couleurs ---
couleurs = set()
trop_sombres = 0
for image, _ in etapes:
for c in pixels_de(image):
couleurs.add(c)
for c in couleurs:
if pixel_eteint_sur_matrice(*c):
trop_sombres += 1
alerte = " ⚠ beaucoup ! anticrénelage ou dégradé flou ?" if len(couleurs) > 64 else " ✓"
print(f" Couleurs : {len(couleurs)} différentes{alerte}")
if trop_sombres:
print(f" Trop sombre : {trop_sombres} couleur(s) seront ÉTEINTES sur la matrice "
f"(à luminosité {LUMINOSITE}) ⚠")
else:
print(" Trop sombre : aucune ✓")
# --- Courant ---
courants = [courant_estime_ma(image) for image, _ in etapes]
maxi = max(courants)
moyen = sum(courants) / len(courants)
limite = " ⚠ limité par le logiciel (image assombrie)" if maxi >= COURANT_MAX_MA - 1 else " ✓"
print(f" Courant : moyen {moyen / 1000:.2f} A, maximum {maxi / 1000:.2f} A{limite}")
# --- Aperçus ---
os.makedirs(DOSSIER_APERCUS, exist_ok=True)
base = os.path.join(DOSSIER_APERCUS, os.path.splitext(nom)[0] + "_apercu")
images = [dessiner_leds(image) for image, _ in etapes]
# Planche : les images les unes sous les autres, numérotées.
montrees = images[:MAX_PLANCHE]
l, h = images[0].size
planche = Image.new("RGB", (l + 50, len(montrees) * (h + 6)), (255, 255, 255))
crayon = ImageDraw.Draw(planche)
for i, im in enumerate(montrees):
planche.paste(im, (50, i * (h + 6)))
crayon.text((6, i * (h + 6) + h // 2 - 6), str(i + 1), fill=(0, 0, 0))
planche.save(base + ".png")
print(f" Aperçu : {base}.png", end="")
if len(images) > 1:
durees = [max(20, round(d * 1000)) for _, d in etapes]
images[0].save(base + ".gif", save_all=True, append_images=images[1:],
duration=durees, loop=0)
print(f" et {base}.gif")
else:
print()
def est_sequence(dossier):
"""Vrai si le dossier ne contient que des PNG (et aucun sous-dossier)."""
noms = [n for n in os.listdir(dossier) if not lecteur.est_cache(n)
and n != "images_par_seconde.txt"]
return bool(noms) and all(n.lower().endswith(".png") and
os.path.isfile(os.path.join(dossier, n)) for n in noms)
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
for chemin in sys.argv[1:]:
# Un dossier peut être une SÉQUENCE (que des PNG, pas de sous-dossier)
# ou un dossier d'animations ENTIER : on vérifie alors chaque élément.
if os.path.isdir(chemin) and not est_sequence(chemin):
cibles = lecteur.lister_animations(chemin)
else:
cibles = [chemin]
for cible in cibles:
try:
verifier(cible)
except Exception as erreur:
print(f" ✗ impossible de lire {cible} : {erreur}")
if __name__ == "__main__":
main()
+188
View File
@@ -0,0 +1,188 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="940" viewBox="0 0 1200 940" font-family="Helvetica, Arial, sans-serif">
<title>Branchement matrice WS2812B 8×32, Raspberry Pi, 74AHCT125 et alimentation d'établi</title>
<defs><marker id="fleche" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="context-stroke"/></marker><marker id="flecheg" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#555"/></marker><marker id="flechec" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#cfcfcf"/></marker></defs>
<rect width="1200" height="940" fill="#ffffff"/>
<text x="60" y="38" font-size="20" fill="#222" text-anchor="start" font-weight="bold">Matrice WS2812B 8 × 32 — vue de dos (côté fils)</text>
<pattern id="pix" x="60" y="55" width="30.625" height="30.625" patternUnits="userSpaceOnUse"><rect x="9" y="9" width="12" height="12" rx="1" fill="#2b2b2b"/></pattern>
<rect x="60" y="55" width="980" height="245" rx="6" fill="#141414"/>
<rect x="60" y="55" width="980" height="245" rx="6" fill="url(#pix)"/>
<line x1="760" y1="150" x2="340" y2="150" stroke="#cfcfcf" stroke-width="3" marker-end="url(#flechec)"/>
<text x="550" y="135" font-size="15" fill="#e6e6e6" text-anchor="middle">les données entrent par DIN et traversent les 256 LED</text>
<text x="110" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">5V</text>
<text x="150" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">GND</text>
<text x="194" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">DOUT</text>
<text x="490" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">5V</text>
<text x="550" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">GND</text>
<text x="880" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">5V</text>
<text x="910" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">GND</text>
<text x="940" y="268" font-size="12" fill="#cfcfcf" text-anchor="middle">DIN</text>
<path d="M110 282 V340" stroke="#d62828" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M150 282 V340" stroke="#bdbdbd" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M190 282 V340" stroke="#2a9d3a" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="96" y="340" width="108" height="24" rx="3" fill="#222"/>
<text x="150" y="390" font-size="14" fill="#555" text-anchor="middle">Sortie DOUT : ne rien brancher</text>
<text x="150" y="408" font-size="13" fill="#888" text-anchor="middle">(sert à chaîner un 2e panneau)</text>
<path d="M490 282 V440 H530 V586" stroke="#d62828" stroke-width="6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M550 282 V420 H590 V586" stroke="#1a1a1a" stroke-width="6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="490" y1="352" x2="506" y2="352" stroke="#777" stroke-width="2"/>
<line x1="534" y1="352" x2="550" y2="352" stroke="#777" stroke-width="2"/>
<rect x="506" y="340" width="28" height="24" rx="5" fill="#1f4e9c"/>
<rect x="524" y="340" width="10" height="24" rx="0" fill="#c9d6ee"/>
<text x="529" y="356" font-size="13" fill="#1f4e9c" text-anchor="middle" font-weight="bold">−</text>
<text x="474" y="340" font-size="14" fill="#1f4e9c" text-anchor="end" font-weight="bold">Condensateur 1000 µF</text>
<text x="474" y="358" font-size="13" fill="#1f4e9c" text-anchor="end">bande « − » côté GND (fil noir)</text>
<text x="474" y="375" font-size="13" fill="#1f4e9c" text-anchor="end">patte + côté 5V (fil rouge)</text>
<rect x="300" y="500" width="330" height="210" rx="10" fill="#e9ecef" stroke="#adb5bd" stroke-width="2"/>
<text x="465" y="526" font-size="16" fill="#333" text-anchor="middle" font-weight="bold">Alimentation d'établi</text>
<rect x="318" y="540" width="160" height="96" rx="4" fill="#1d2b1d"/>
<text x="398" y="578" font-size="28" fill="#7dff7d" text-anchor="middle" font-weight="bold" font-family="monospace">5.00 V</text>
<text x="398" y="618" font-size="20" fill="#ffb347" text-anchor="middle" font-family="monospace">3.00 A</text>
<circle cx="345" cy="672" r="16" fill="#666"/>
<text x="345" y="702" font-size="11" fill="#555" text-anchor="middle">tension</text>
<circle cx="420" cy="672" r="16" fill="#666"/>
<text x="420" y="702" font-size="11" fill="#555" text-anchor="middle">courant</text>
<circle cx="530" cy="600" r="13" fill="#d62828" stroke="#7a0f0f" stroke-width="2"/>
<circle cx="590" cy="600" r="13" fill="#1a1a1a" stroke="#000" stroke-width="2"/>
<text x="530" y="637" font-size="15" fill="#d62828" text-anchor="middle" font-weight="bold">+</text>
<text x="590" y="637" font-size="15" fill="#1a1a1a" text-anchor="middle" font-weight="bold">−</text>
<text x="560" y="660" font-size="12" fill="#555" text-anchor="middle">bornes de</text>
<text x="560" y="675" font-size="12" fill="#555" text-anchor="middle">sortie</text>
<path d="M880 282 V340" stroke="#d62828" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M910 282 V340" stroke="#bdbdbd" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M940 282 V340" stroke="#2a9d3a" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="866" y="340" width="88" height="22" rx="3" fill="#222"/>
<rect x="870" y="364" width="80" height="22" rx="3" fill="#3a3a3a"/>
<text x="966" y="358" font-size="13" fill="#555" text-anchor="start">connecteur DIN de la matrice</text>
<text x="966" y="381" font-size="13" fill="#555" text-anchor="start">+ fiche fournie (rouge / blanc / vert)</text>
<rect x="700" y="425" width="440" height="190" rx="8" fill="#f6f1e3" stroke="#d9cfb2" stroke-width="2"/>
<text x="1132" y="608" font-size="12" fill="#a8997a" text-anchor="end">plaque d'essai</text>
<path d="M880 386 V444 H815 V462" stroke="#d62828" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M910 386 V454 H822 a7 7 0 0 0 -14 0 H780 V585" stroke="#bdbdbd" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M940 386 V444 H1080 V470" stroke="#2a9d3a" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1080 510 V552 H990 a7 7 0 0 0 -14 0 H871 V528" stroke="#2a9d3a" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M780 585 H836 a7 7 0 0 1 14 0 H983" stroke="#1a1a1a" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M815 528 V585" stroke="#1a1a1a" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M983 528 V585" stroke="#1a1a1a" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="780" cy="585" r="5" fill="#1a1a1a"/>
<circle cx="815" cy="585" r="5" fill="#1a1a1a"/>
<circle cx="983" cy="585" r="5" fill="#1a1a1a"/>
<rect x="805" y="470" width="192" height="50" rx="4" fill="#222"/>
<path d="M805 485 a10 10 0 0 1 0 20" fill="#555"/>
<text x="901" y="500" font-size="15" fill="#eee" text-anchor="middle" font-weight="bold">74AHCT125</text>
<rect x="810" y="520" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="815" y="515" font-size="9" fill="#bbb" text-anchor="middle">1</text>
<rect x="838" y="520" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="843" y="515" font-size="9" fill="#bbb" text-anchor="middle">2</text>
<rect x="866" y="520" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="871" y="515" font-size="9" fill="#bbb" text-anchor="middle">3</text>
<rect x="894" y="520" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="899" y="515" font-size="9" fill="#bbb" text-anchor="middle">4</text>
<rect x="922" y="520" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="927" y="515" font-size="9" fill="#bbb" text-anchor="middle">5</text>
<rect x="950" y="520" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="955" y="515" font-size="9" fill="#bbb" text-anchor="middle">6</text>
<rect x="978" y="520" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="983" y="515" font-size="9" fill="#bbb" text-anchor="middle">7</text>
<rect x="978" y="462" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="983" y="482" font-size="9" fill="#bbb" text-anchor="middle">8</text>
<rect x="950" y="462" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="955" y="482" font-size="9" fill="#bbb" text-anchor="middle">9</text>
<rect x="922" y="462" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="927" y="482" font-size="9" fill="#bbb" text-anchor="middle">10</text>
<rect x="894" y="462" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="899" y="482" font-size="9" fill="#bbb" text-anchor="middle">11</text>
<rect x="866" y="462" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="871" y="482" font-size="9" fill="#bbb" text-anchor="middle">12</text>
<rect x="838" y="462" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="843" y="482" font-size="9" fill="#bbb" text-anchor="middle">13</text>
<rect x="810" y="462" width="10" height="8" rx="0" fill="#b8b8b8"/>
<text x="815" y="482" font-size="9" fill="#bbb" text-anchor="middle">14</text>
<text x="805" y="545" font-size="11" fill="#333" text-anchor="end" font-weight="bold">1OE</text>
<text x="849" y="545" font-size="11" fill="#e0a800" text-anchor="start" font-weight="bold">1A</text>
<text x="877" y="545" font-size="11" fill="#2a9d3a" text-anchor="start" font-weight="bold">1Y</text>
<text x="991" y="545" font-size="11" fill="#333" text-anchor="start" font-weight="bold">GND</text>
<rect x="1071" y="470" width="18" height="40" rx="6" fill="#e8d3a8" stroke="#a88a52" stroke-width="1"/>
<rect x="1071" y="478" width="18" height="4" rx="0" fill="#e67e22"/>
<rect x="1071" y="486" width="18" height="4" rx="0" fill="#e67e22"/>
<rect x="1071" y="494" width="18" height="4" rx="0" fill="#8b4513"/>
<rect x="1071" y="502" width="18" height="3" rx="0" fill="#c9a227"/>
<text x="1098" y="486" font-size="13" fill="#7a5a1e" text-anchor="start" font-weight="bold">330 Ω</text>
<text x="966" y="404" font-size="12" fill="#d62828" text-anchor="start" font-weight="bold">fil rouge de la fiche : il alimente</text>
<text x="966" y="418" font-size="12" fill="#d62828" text-anchor="start" font-weight="bold">la puce (patte 14 = VCC, 5 V)</text>
<rect x="700" y="645" width="440" height="230" rx="12" fill="#1f7a3a"/>
<rect x="750" y="655" width="336" height="42" rx="3" fill="#143d20"/>
<rect x="758" y="661" width="10" height="10" rx="1" fill="#d62828"/>
<rect x="758" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="774" y="661" width="10" height="10" rx="1" fill="#d62828"/>
<rect x="774" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="790" y="661" width="10" height="10" rx="1" fill="#1a1a1a"/>
<rect x="790" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="806" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="806" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="822" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="822" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="838" y="661" width="10" height="10" rx="1" fill="#e0a800"/>
<rect x="838" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="854" y="661" width="10" height="10" rx="1" fill="#1a1a1a"/>
<rect x="854" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="870" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="870" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="886" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="886" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="902" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="902" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="918" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="918" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="934" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="934" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="950" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="950" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="966" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="966" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="982" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="982" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="998" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="998" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1014" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1014" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1030" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1030" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1046" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1046" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1062" y="661" width="10" height="10" rx="1" fill="#d4af37"/>
<rect x="1062" y="679" width="10" height="10" rx="1" fill="#d4af37"/>
<text x="754" y="670" font-size="9" fill="#cde" text-anchor="end">2</text>
<text x="754" y="688" font-size="9" fill="#cde" text-anchor="end">1</text>
<text x="1076" y="670" font-size="9" fill="#cde" text-anchor="start">40</text>
<text x="1076" y="688" font-size="9" fill="#cde" text-anchor="start">39</text>
<path d="M843 528 V661" stroke="#e0a800" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M780 585 V630 H795 V661" stroke="#1a1a1a" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="920" y="770" font-size="24" fill="#fff" text-anchor="middle" font-weight="bold">Raspberry Pi</text>
<text x="920" y="794" font-size="14" fill="#d8f0dd" text-anchor="middle">3B+, 4 ou Zero 2 W (pas le Pi 5)</text>
<text x="843" y="712" font-size="12" fill="#ffe08a" text-anchor="start" font-weight="bold">patte 12 = GPIO 18</text>
<text x="793" y="712" font-size="12" fill="#fff" text-anchor="end" font-weight="bold">patte 6 = GND</text>
<path d="M771 833 L787 849 M787 833 L771 849" stroke="#ffb3b3" stroke-width="3"/>
<text x="797" y="846" font-size="12" fill="#ffd6d6" text-anchor="start">pattes 2 et 4 (5 V) : NE PAS relier à la matrice</text>
<rect x="1140" y="790" width="18" height="34" rx="3" fill="#b8b8b8"/>
<path d="M1158 807 H1180 V900" stroke="#555" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="1170" y="918" font-size="13" fill="#333" text-anchor="end">bloc secteur USB du Pi</text>
<text x="1170" y="935" font-size="12" fill="#777" text-anchor="end">(5 V, 2,5 A ou plus)</text>
<rect x="40" y="470" width="236" height="300" rx="8" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
<text x="56" y="496" font-size="15" fill="#222" text-anchor="start" font-weight="bold">Code couleur des fils</text>
<line x1="56" y1="520" x2="90" y2="520" stroke="#d62828" stroke-width="6" stroke-linecap="round"/>
<text x="100" y="525" font-size="14" fill="#333" text-anchor="start">rouge = + 5 V</text>
<line x1="56" y1="548" x2="90" y2="548" stroke="#1a1a1a" stroke-width="6" stroke-linecap="round"/>
<text x="100" y="553" font-size="14" fill="#333" text-anchor="start">noir = GND (masse)</text>
<line x1="56" y1="576" x2="90" y2="576" stroke="#bdbdbd" stroke-width="6" stroke-linecap="round"/>
<text x="100" y="581" font-size="14" fill="#333" text-anchor="start">blanc = GND (masse)</text>
<line x1="56" y1="604" x2="90" y2="604" stroke="#2a9d3a" stroke-width="6" stroke-linecap="round"/>
<text x="100" y="609" font-size="14" fill="#333" text-anchor="start">vert = données 5 V</text>
<line x1="56" y1="632" x2="90" y2="632" stroke="#e0a800" stroke-width="6" stroke-linecap="round"/>
<text x="100" y="637" font-size="14" fill="#333" text-anchor="start">jaune = données 3,3 V</text>
<text x="56" y="670" font-size="13" fill="#555" text-anchor="start">Tous les GND sont reliés :</text>
<text x="56" y="688" font-size="13" fill="#555" text-anchor="start">Pi ↔ puce ↔ fil blanc ↔ matrice</text>
<text x="56" y="706" font-size="13" fill="#555" text-anchor="start">↔ fil noir ↔ borne − de l'alim.</text>
<text x="56" y="730" font-size="13" fill="#b00020" text-anchor="start" font-weight="bold">Alim. réglée sur 5,0 V maxi !</text>
<text x="56" y="752" font-size="12" fill="#777" text-anchor="start">Un pont = les fils se croisent</text>
<text x="56" y="766" font-size="12" fill="#777" text-anchor="start">sans se toucher.</text>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

+62
View File
@@ -0,0 +1,62 @@
<svg xmlns="http://www.w3.org/2000/svg" width="820" height="470" viewBox="0 0 820 470" font-family="Helvetica, Arial, sans-serif">
<title>Brochage du 74AHCT125 (vu de dessus)</title>
<defs><marker id="fleche" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="context-stroke"/></marker><marker id="flecheg" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#555"/></marker><marker id="flechec" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#cfcfcf"/></marker></defs>
<rect width="820" height="470" fill="#ffffff"/>
<text x="410" y="36" font-size="20" fill="#222" text-anchor="middle" font-weight="bold">74AHCT125 vu de DESSUS (encoche à gauche)</text>
<rect x="250" y="150" width="320" height="110" rx="6" fill="#222"/>
<path d="M250 185 a20 20 0 0 1 0 40" fill="#555"/>
<circle cx="275" cy="240" r="6" fill="#555"/>
<text x="410" y="212" font-size="24" fill="#eee" text-anchor="middle" font-weight="bold">74AHCT125</text>
<rect x="272" y="260" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="280" y="252" font-size="12" fill="#bbb" text-anchor="middle">1</text>
<text x="280" y="306" font-size="14" fill="#333" text-anchor="middle" font-weight="bold">1OE</text>
<line x1="280" y1="312" x2="280" y2="325" stroke="#333" stroke-width="1.5"/>
<text x="280" y="338" font-size="13" fill="#333" text-anchor="middle" font-weight="bold">→ GND</text>
<rect x="315" y="260" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="323" y="252" font-size="12" fill="#bbb" text-anchor="middle">2</text>
<text x="323" y="306" font-size="14" fill="#e0a800" text-anchor="middle" font-weight="bold">1A</text>
<line x1="323" y1="312" x2="323" y2="347" stroke="#e0a800" stroke-width="1.5"/>
<text x="323" y="360" font-size="13" fill="#e0a800" text-anchor="middle" font-weight="bold">← GPIO 18</text>
<rect x="358" y="260" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="366" y="252" font-size="12" fill="#bbb" text-anchor="middle">3</text>
<text x="366" y="306" font-size="14" fill="#2a9d3a" text-anchor="middle" font-weight="bold">1Y</text>
<line x1="366" y1="312" x2="366" y2="369" stroke="#2a9d3a" stroke-width="1.5"/>
<text x="366" y="382" font-size="13" fill="#2a9d3a" text-anchor="middle" font-weight="bold">→ 330 Ω → DIN</text>
<rect x="401" y="260" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="409" y="252" font-size="12" fill="#bbb" text-anchor="middle">4</text>
<text x="409" y="306" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">2OE</text>
<rect x="444" y="260" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="452" y="252" font-size="12" fill="#bbb" text-anchor="middle">5</text>
<text x="452" y="306" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">2A</text>
<rect x="487" y="260" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="495" y="252" font-size="12" fill="#bbb" text-anchor="middle">6</text>
<text x="495" y="306" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">2Y</text>
<rect x="530" y="260" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="538" y="252" font-size="12" fill="#bbb" text-anchor="middle">7</text>
<text x="538" y="306" font-size="14" fill="#333" text-anchor="middle" font-weight="bold">GND</text>
<line x1="538" y1="312" x2="538" y2="325" stroke="#333" stroke-width="1.5"/>
<text x="538" y="338" font-size="13" fill="#333" text-anchor="middle" font-weight="bold">→ GND</text>
<rect x="272" y="124" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="280" y="168" font-size="12" fill="#bbb" text-anchor="middle">14</text>
<text x="280" y="114" font-size="14" fill="#d62828" text-anchor="middle" font-weight="bold">VCC</text>
<text x="280" y="92" font-size="13" fill="#d62828" text-anchor="middle" font-weight="bold">← 5 V</text>
<rect x="315" y="124" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="323" y="168" font-size="12" fill="#bbb" text-anchor="middle">13</text>
<text x="323" y="114" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">4OE</text>
<rect x="358" y="124" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="366" y="168" font-size="12" fill="#bbb" text-anchor="middle">12</text>
<text x="366" y="114" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">4A</text>
<rect x="401" y="124" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="409" y="168" font-size="12" fill="#bbb" text-anchor="middle">11</text>
<text x="409" y="114" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">4Y</text>
<rect x="444" y="124" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="452" y="168" font-size="12" fill="#bbb" text-anchor="middle">10</text>
<text x="452" y="114" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">3OE</text>
<rect x="487" y="124" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="495" y="168" font-size="12" fill="#bbb" text-anchor="middle">9</text>
<text x="495" y="114" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">3A</text>
<rect x="530" y="124" width="16" height="26" rx="0" fill="#b8b8b8"/>
<text x="538" y="168" font-size="12" fill="#bbb" text-anchor="middle">8</text>
<text x="538" y="114" font-size="14" fill="#aaa" text-anchor="middle" font-weight="bold">3Y</text>
<text x="40" y="445" font-size="13" fill="#555" text-anchor="start">On n'utilise qu'une des 4 « portes » (la n° 1). Pattes en gris : non branchées (on peut relier 4, 10 et 13 au 5 V pour les désactiver).</text>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

+55
View File
@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="330" viewBox="0 0 1200 330" font-family="Helvetica, Arial, sans-serif">
<title>De Krita à la matrice : le trajet d'une animation</title>
<defs><marker id="fleche" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="context-stroke"/></marker><marker id="flecheg" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#555"/></marker><marker id="flechec" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M1 1 L9 5 L1 9 Z" fill="#cfcfcf"/></marker></defs>
<rect width="1200" height="330" fill="#ffffff"/>
<rect x="30" y="60" width="170" height="190" rx="12" fill="#ffffff" stroke="#7b3fa0" stroke-width="3"/>
<rect x="30" y="60" width="170" height="46" rx="12" fill="#7b3fa0"/>
<rect x="30" y="90" width="170" height="16" rx="0" fill="#7b3fa0"/>
<text x="115.0" y="90" font-size="18" fill="#fff" text-anchor="middle" font-weight="bold">1. Dessiner</text>
<text x="115.0" y="140" font-size="14" fill="#222" text-anchor="middle" font-weight="bold">Krita</text>
<text x="115.0" y="166" font-size="13" fill="#555" text-anchor="middle">document 32 × 8 px</text>
<text x="115.0" y="192" font-size="13" fill="#555" text-anchor="middle">pixel art, sans flou</text>
<line x1="204" y1="155" x2="224" y2="155" stroke="#555" stroke-width="3" marker-end="url(#flecheg)"/>
<rect x="228" y="60" width="170" height="190" rx="12" fill="#ffffff" stroke="#b5418f" stroke-width="3"/>
<rect x="228" y="60" width="170" height="46" rx="12" fill="#b5418f"/>
<rect x="228" y="90" width="170" height="16" rx="0" fill="#b5418f"/>
<text x="313.0" y="90" font-size="18" fill="#fff" text-anchor="middle" font-weight="bold">2. Exporter</text>
<text x="313.0" y="140" font-size="14" fill="#222" text-anchor="middle" font-weight="bold">GIF animé</text>
<text x="313.0" y="166" font-size="13" fill="#555" text-anchor="middle">ou séquence de PNG</text>
<text x="313.0" y="192" font-size="13" fill="#555" text-anchor="middle">(Rendu de l'animation)</text>
<line x1="402" y1="155" x2="422" y2="155" stroke="#555" stroke-width="3" marker-end="url(#flecheg)"/>
<rect x="426" y="60" width="170" height="190" rx="12" fill="#ffffff" stroke="#c9682a" stroke-width="3"/>
<rect x="426" y="60" width="170" height="46" rx="12" fill="#c9682a"/>
<rect x="426" y="90" width="170" height="16" rx="0" fill="#c9682a"/>
<text x="511.0" y="90" font-size="18" fill="#fff" text-anchor="middle" font-weight="bold">3. Vérifier</text>
<text x="511.0" y="140" font-size="14" fill="#222" text-anchor="middle" font-weight="bold">verifier_animation.py</text>
<text x="511.0" y="166" font-size="13" fill="#555" text-anchor="middle">+ simulateur</text>
<text x="511.0" y="192" font-size="13" fill="#555" text-anchor="middle">sur l'ordinateur</text>
<line x1="600" y1="155" x2="620" y2="155" stroke="#555" stroke-width="3" marker-end="url(#flecheg)"/>
<rect x="624" y="60" width="170" height="190" rx="12" fill="#ffffff" stroke="#b08900" stroke-width="3"/>
<rect x="624" y="60" width="170" height="46" rx="12" fill="#b08900"/>
<rect x="624" y="90" width="170" height="16" rx="0" fill="#b08900"/>
<text x="709.0" y="90" font-size="18" fill="#fff" text-anchor="middle" font-weight="bold">4. Déposer</text>
<text x="709.0" y="140" font-size="14" fill="#222" text-anchor="middle" font-weight="bold">dossier « animations »</text>
<text x="709.0" y="166" font-size="13" fill="#555" text-anchor="middle">carte SD ou réseau</text>
<text x="709.0" y="192" font-size="13" fill="#555" text-anchor="middle">(01_, 02_… = ordre)</text>
<line x1="798" y1="155" x2="818" y2="155" stroke="#555" stroke-width="3" marker-end="url(#flecheg)"/>
<rect x="822" y="60" width="170" height="190" rx="12" fill="#ffffff" stroke="#1f7a3a" stroke-width="3"/>
<rect x="822" y="60" width="170" height="46" rx="12" fill="#1f7a3a"/>
<rect x="822" y="90" width="170" height="16" rx="0" fill="#1f7a3a"/>
<text x="907.0" y="90" font-size="18" fill="#fff" text-anchor="middle" font-weight="bold">5. Lire</text>
<text x="907.0" y="140" font-size="14" fill="#222" text-anchor="middle" font-weight="bold">Raspberry Pi</text>
<text x="907.0" y="166" font-size="13" fill="#555" text-anchor="middle">lecteur.py : gamma,</text>
<text x="907.0" y="192" font-size="13" fill="#555" text-anchor="middle">luminosité, serpentin</text>
<line x1="996" y1="155" x2="1016" y2="155" stroke="#555" stroke-width="3" marker-end="url(#flecheg)"/>
<rect x="1020" y="60" width="170" height="190" rx="12" fill="#ffffff" stroke="#2f6fc4" stroke-width="3"/>
<rect x="1020" y="60" width="170" height="46" rx="12" fill="#2f6fc4"/>
<rect x="1020" y="90" width="170" height="16" rx="0" fill="#2f6fc4"/>
<text x="1105.0" y="90" font-size="18" fill="#fff" text-anchor="middle" font-weight="bold">6. Afficher</text>
<text x="1105.0" y="140" font-size="14" fill="#222" text-anchor="middle" font-weight="bold">74AHCT125 (3,3 → 5 V)</text>
<text x="1105.0" y="166" font-size="13" fill="#555" text-anchor="middle">puis 256 LED</text>
<text x="1105.0" y="192" font-size="13" fill="#555" text-anchor="middle">WS2812B</text>
<text x="30" y="36" font-size="22" fill="#222" text-anchor="start" font-weight="bold">De Krita à la matrice</text>
<path d="M115 270 V292 H1003 V270" stroke="#aaa" stroke-width="2" fill="none" stroke-dasharray="6 5"/>
<text x="559" y="316" font-size="14" fill="#555" text-anchor="middle">Étapes 1 à 4 : l'équipe design, sur n'importe quel ordinateur. Étapes 5 et 6 : automatiques, dans la vitrine.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

+85
View File
@@ -0,0 +1,85 @@
"""
=============================================================================
TEST DE LA MATRICE — branchement, orientation, alimentation
Workshop « Vitrine pixels »
=============================================================================
À lancer EN PREMIER, après chaque nouveau montage :
sudo ~/vitrine-env/bin/python test_matrice.py
(ou, pour voir à quoi ça ressemble sans matériel :
python3 test_matrice.py --simulateur)
Trois tests s'enchaînent, puis tout s'éteint :
1. LES COINS : un pixel de couleur dans chaque coin, 5 secondes.
ROUGE en haut à gauche VERT en haut à droite
BLEU en bas à gauche BLANC en bas à droite
Sinon : changez INVERSER_X / INVERSER_Y dans matrice.py.
2. L'ORDRE DE LA CHAÎNE : un point orange parcourt les 256 LED
dans l'ordre où le signal les traverse. On voit le serpentin.
Si le point s'arrête en route : une LED ou une soudure est abîmée
à cet endroit (tout ce qui suit reste éteint).
3. LES COULEURS PLEINES : tout en rouge, puis vert, puis bleu, puis
blanc. Vérifie l'ordre des couleurs (GRB) et l'alimentation.
Le blanc est le plus gourmand : si la matrice devient orangée ou
clignote, l'alimentation est à sa limite (voir README, partie 10).
"""
import matrice
from matrice import LARGEUR, HAUTEUR, NB_LEDS, ROUGE, VERT, BLEU, BLANC, ORANGE
def test_coins():
print("1. Test des coins : rouge ↖ vert ↗ bleu ↙ blanc ↘")
matrice.effacer()
matrice.pixel(0, 0, ROUGE)
matrice.pixel(LARGEUR - 1, 0, VERT)
matrice.pixel(0, HAUTEUR - 1, BLEU)
matrice.pixel(LARGEUR - 1, HAUTEUR - 1, BLANC)
matrice.montrer()
matrice.attendre(5000)
def test_ordre_des_leds():
print("2. Ordre de la chaîne : le point orange suit le serpentin")
# Pour suivre l'ordre de la CHAÎNE (et pas l'ordre des coordonnées),
# on cherche, pour chaque numéro de LED, la position (x, y) qui lui
# correspond grâce à XY().
position = {}
for x in range(LARGEUR):
for y in range(HAUTEUR):
position[matrice.XY(x, y)] = (x, y)
matrice.effacer()
for numero in range(NB_LEDS):
x, y = position[numero]
matrice.pixel(x, y, ORANGE) # la tête du serpent
matrice.montrer()
matrice.pixel(x, y, (60, 20, 0)) # laisse une trace plus sombre
matrice.attendre(15)
matrice.attendre(1000)
def test_couleurs_pleines():
print("3. Couleurs pleines : rouge, vert, bleu, blanc")
for nom, couleur in [("rouge", ROUGE), ("vert", VERT), ("bleu", BLEU), ("blanc", BLANC)]:
print(" tout en", nom)
matrice.effacer(couleur)
matrice.montrer()
matrice.attendre(2000)
if __name__ == "__main__":
matrice.demarrer()
try:
test_coins()
test_ordre_des_leds()
test_couleurs_pleines()
print("Terminé.")
except KeyboardInterrupt:
print("\nArrêt demandé.")
finally:
matrice.eteindre()