diff --git a/8x32 PI/.gitignore b/8x32 PI/.gitignore new file mode 100644 index 0000000..13bdde1 --- /dev/null +++ b/8x32 PI/.gitignore @@ -0,0 +1,2 @@ +apercus/ +__pycache__/ diff --git a/8x32 PI/CODER_EN_PYTHON.md b/8x32 PI/CODER_EN_PYTHON.md new file mode 100644 index 0000000..56bd7de --- /dev/null +++ b/8x32 PI/CODER_EN_PYTHON.md @@ -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**, , 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 : +. + +--- + +## 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`). diff --git a/8x32 PI/GUIDE_KRITA.md b/8x32 PI/GUIDE_KRITA.md new file mode 100644 index 0000000..ea43de4 --- /dev/null +++ b/8x32 PI/GUIDE_KRITA.md @@ -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 () 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** | (cochez **« Add python.exe to PATH »**) | `py -m pip install pillow` | +| **macOS** | | `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 diff --git a/8x32 PI/INSTALLATION_PI.md b/8x32 PI/INSTALLATION_PI.md new file mode 100644 index 0000000..eb13c16 --- /dev/null +++ b/8x32 PI/INSTALLATION_PI.md @@ -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 : +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 | diff --git a/8x32 PI/MODE_VITRINE.md b/8x32 PI/MODE_VITRINE.md new file mode 100644 index 0000000..1520ffe --- /dev/null +++ b/8x32 PI/MODE_VITRINE.md @@ -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) | diff --git a/8x32 PI/README.md b/8x32 PI/README.md new file mode 100644 index 0000000..31d26eb --- /dev/null +++ b/8x32 PI/README.md @@ -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. diff --git a/8x32 PI/animations/01_coeur_qui_bat.gif b/8x32 PI/animations/01_coeur_qui_bat.gif new file mode 100644 index 0000000..cd2c790 Binary files /dev/null and b/8x32 PI/animations/01_coeur_qui_bat.gif differ diff --git a/8x32 PI/animations/02_texte_defilant.gif b/8x32 PI/animations/02_texte_defilant.gif new file mode 100644 index 0000000..0167880 Binary files /dev/null and b/8x32 PI/animations/02_texte_defilant.gif differ diff --git a/8x32 PI/animations/03_vague/images_par_seconde.txt b/8x32 PI/animations/03_vague/images_par_seconde.txt new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/8x32 PI/animations/03_vague/images_par_seconde.txt @@ -0,0 +1 @@ +20 diff --git a/8x32 PI/animations/03_vague/vague_0000.png b/8x32 PI/animations/03_vague/vague_0000.png new file mode 100644 index 0000000..0adfd74 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0000.png differ diff --git a/8x32 PI/animations/03_vague/vague_0001.png b/8x32 PI/animations/03_vague/vague_0001.png new file mode 100644 index 0000000..fe42bde Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0001.png differ diff --git a/8x32 PI/animations/03_vague/vague_0002.png b/8x32 PI/animations/03_vague/vague_0002.png new file mode 100644 index 0000000..aa89e9b Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0002.png differ diff --git a/8x32 PI/animations/03_vague/vague_0003.png b/8x32 PI/animations/03_vague/vague_0003.png new file mode 100644 index 0000000..8dbdec5 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0003.png differ diff --git a/8x32 PI/animations/03_vague/vague_0004.png b/8x32 PI/animations/03_vague/vague_0004.png new file mode 100644 index 0000000..a651eb5 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0004.png differ diff --git a/8x32 PI/animations/03_vague/vague_0005.png b/8x32 PI/animations/03_vague/vague_0005.png new file mode 100644 index 0000000..41fa19d Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0005.png differ diff --git a/8x32 PI/animations/03_vague/vague_0006.png b/8x32 PI/animations/03_vague/vague_0006.png new file mode 100644 index 0000000..a58010b Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0006.png differ diff --git a/8x32 PI/animations/03_vague/vague_0007.png b/8x32 PI/animations/03_vague/vague_0007.png new file mode 100644 index 0000000..32eaf88 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0007.png differ diff --git a/8x32 PI/animations/03_vague/vague_0008.png b/8x32 PI/animations/03_vague/vague_0008.png new file mode 100644 index 0000000..61400d2 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0008.png differ diff --git a/8x32 PI/animations/03_vague/vague_0009.png b/8x32 PI/animations/03_vague/vague_0009.png new file mode 100644 index 0000000..c1b3f53 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0009.png differ diff --git a/8x32 PI/animations/03_vague/vague_0010.png b/8x32 PI/animations/03_vague/vague_0010.png new file mode 100644 index 0000000..5766e6d Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0010.png differ diff --git a/8x32 PI/animations/03_vague/vague_0011.png b/8x32 PI/animations/03_vague/vague_0011.png new file mode 100644 index 0000000..ed0e671 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0011.png differ diff --git a/8x32 PI/animations/03_vague/vague_0012.png b/8x32 PI/animations/03_vague/vague_0012.png new file mode 100644 index 0000000..95a461c Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0012.png differ diff --git a/8x32 PI/animations/03_vague/vague_0013.png b/8x32 PI/animations/03_vague/vague_0013.png new file mode 100644 index 0000000..f70f24a Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0013.png differ diff --git a/8x32 PI/animations/03_vague/vague_0014.png b/8x32 PI/animations/03_vague/vague_0014.png new file mode 100644 index 0000000..e52013e Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0014.png differ diff --git a/8x32 PI/animations/03_vague/vague_0015.png b/8x32 PI/animations/03_vague/vague_0015.png new file mode 100644 index 0000000..1cb8b95 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0015.png differ diff --git a/8x32 PI/animations/03_vague/vague_0016.png b/8x32 PI/animations/03_vague/vague_0016.png new file mode 100644 index 0000000..4c910a5 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0016.png differ diff --git a/8x32 PI/animations/03_vague/vague_0017.png b/8x32 PI/animations/03_vague/vague_0017.png new file mode 100644 index 0000000..9f2f3e3 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0017.png differ diff --git a/8x32 PI/animations/03_vague/vague_0018.png b/8x32 PI/animations/03_vague/vague_0018.png new file mode 100644 index 0000000..52d74f4 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0018.png differ diff --git a/8x32 PI/animations/03_vague/vague_0019.png b/8x32 PI/animations/03_vague/vague_0019.png new file mode 100644 index 0000000..0c9943e Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0019.png differ diff --git a/8x32 PI/animations/03_vague/vague_0020.png b/8x32 PI/animations/03_vague/vague_0020.png new file mode 100644 index 0000000..06441ed Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0020.png differ diff --git a/8x32 PI/animations/03_vague/vague_0021.png b/8x32 PI/animations/03_vague/vague_0021.png new file mode 100644 index 0000000..79e307a Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0021.png differ diff --git a/8x32 PI/animations/03_vague/vague_0022.png b/8x32 PI/animations/03_vague/vague_0022.png new file mode 100644 index 0000000..384d9e7 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0022.png differ diff --git a/8x32 PI/animations/03_vague/vague_0023.png b/8x32 PI/animations/03_vague/vague_0023.png new file mode 100644 index 0000000..7f4808d Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0023.png differ diff --git a/8x32 PI/animations/03_vague/vague_0024.png b/8x32 PI/animations/03_vague/vague_0024.png new file mode 100644 index 0000000..63ff437 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0024.png differ diff --git a/8x32 PI/animations/03_vague/vague_0025.png b/8x32 PI/animations/03_vague/vague_0025.png new file mode 100644 index 0000000..6334dce Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0025.png differ diff --git a/8x32 PI/animations/03_vague/vague_0026.png b/8x32 PI/animations/03_vague/vague_0026.png new file mode 100644 index 0000000..1e9bf87 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0026.png differ diff --git a/8x32 PI/animations/03_vague/vague_0027.png b/8x32 PI/animations/03_vague/vague_0027.png new file mode 100644 index 0000000..f478e3c Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0027.png differ diff --git a/8x32 PI/animations/03_vague/vague_0028.png b/8x32 PI/animations/03_vague/vague_0028.png new file mode 100644 index 0000000..b0e479f Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0028.png differ diff --git a/8x32 PI/animations/03_vague/vague_0029.png b/8x32 PI/animations/03_vague/vague_0029.png new file mode 100644 index 0000000..3f653b3 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0029.png differ diff --git a/8x32 PI/animations/03_vague/vague_0030.png b/8x32 PI/animations/03_vague/vague_0030.png new file mode 100644 index 0000000..8ffd215 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0030.png differ diff --git a/8x32 PI/animations/03_vague/vague_0031.png b/8x32 PI/animations/03_vague/vague_0031.png new file mode 100644 index 0000000..c03aa1d Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0031.png differ diff --git a/8x32 PI/animations/03_vague/vague_0032.png b/8x32 PI/animations/03_vague/vague_0032.png new file mode 100644 index 0000000..3f00d1f Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0032.png differ diff --git a/8x32 PI/animations/03_vague/vague_0033.png b/8x32 PI/animations/03_vague/vague_0033.png new file mode 100644 index 0000000..f1e47aa Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0033.png differ diff --git a/8x32 PI/animations/03_vague/vague_0034.png b/8x32 PI/animations/03_vague/vague_0034.png new file mode 100644 index 0000000..5a5e519 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0034.png differ diff --git a/8x32 PI/animations/03_vague/vague_0035.png b/8x32 PI/animations/03_vague/vague_0035.png new file mode 100644 index 0000000..bb107f6 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0035.png differ diff --git a/8x32 PI/animations/03_vague/vague_0036.png b/8x32 PI/animations/03_vague/vague_0036.png new file mode 100644 index 0000000..cac54de Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0036.png differ diff --git a/8x32 PI/animations/03_vague/vague_0037.png b/8x32 PI/animations/03_vague/vague_0037.png new file mode 100644 index 0000000..e2e2712 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0037.png differ diff --git a/8x32 PI/animations/03_vague/vague_0038.png b/8x32 PI/animations/03_vague/vague_0038.png new file mode 100644 index 0000000..1afa413 Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0038.png differ diff --git a/8x32 PI/animations/03_vague/vague_0039.png b/8x32 PI/animations/03_vague/vague_0039.png new file mode 100644 index 0000000..f848a9e Binary files /dev/null and b/8x32 PI/animations/03_vague/vague_0039.png differ diff --git a/8x32 PI/animations/04_balle.png b/8x32 PI/animations/04_balle.png new file mode 100644 index 0000000..892d737 Binary files /dev/null and b/8x32 PI/animations/04_balle.png differ diff --git a/8x32 PI/installation/installer.sh b/8x32 PI/installation/installer.sh new file mode 100755 index 0000000..455b015 --- /dev/null +++ b/8x32 PI/installation/installer.sh @@ -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 "================================================================" diff --git a/8x32 PI/installation/vitrine-pixels.service b/8x32 PI/installation/vitrine-pixels.service new file mode 100644 index 0000000..90ec0e0 --- /dev/null +++ b/8x32 PI/installation/vitrine-pixels.service @@ -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 diff --git a/8x32 PI/krita/palette_vitrine_pixels.gpl b/8x32 PI/krita/palette_vitrine_pixels.gpl new file mode 100644 index 0000000..7cbfc2a --- /dev/null +++ b/8x32 PI/krita/palette_vitrine_pixels.gpl @@ -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 diff --git a/8x32 PI/lecteur.py b/8x32 PI/lecteur.py new file mode 100644 index 0000000..1ad2007 --- /dev/null +++ b/8x32 PI/lecteur.py @@ -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() diff --git a/8x32 PI/matrice.py b/8x32 PI/matrice.py new file mode 100644 index 0000000..a5ca469 --- /dev/null +++ b/8x32 PI/matrice.py @@ -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() diff --git a/8x32 PI/mes_animations.py b/8x32 PI/mes_animations.py new file mode 100644 index 0000000..2bf6372 --- /dev/null +++ b/8x32 PI/mes_animations.py @@ -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() diff --git a/8x32 PI/outils/generer_exemples.py b/8x32 PI/outils/generer_exemples.py new file mode 100755 index 0000000..7c9d878 --- /dev/null +++ b/8x32 PI/outils/generer_exemples.py @@ -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")) diff --git a/8x32 PI/outils/schemas.py b/8x32 PI/outils/schemas.py new file mode 100755 index 0000000..491e5de --- /dev/null +++ b/8x32 PI/outils/schemas.py @@ -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'', + f' {titre}', + ' ' + '' + '' + '', + f' '] + + 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'') + + 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'{s}') + + def fil(self, d, c, w=5): + self.add(f'') + + def point(self, x, y, c, r=5): + self.add(f'') + + def save(self, nom): + self.out.append("") + 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('') + s.rect(60, 55, 980, 245, "#141414", 6) + s.rect(60, 55, 980, 245, "url(#pix)", 6) + s.add('') + 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('') + s.add('') + 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'') + s.texte(x, 702, t, 11, "#555", "middle") + s.add(f'') + s.add(f'') + 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('') + 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'') + 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'') + 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('') + s.add('') + 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'') + 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'') + s.texte(30, 36, "De Krita à la matrice", 22, "#222", gras=True) + s.add('') + 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) diff --git a/8x32 PI/outils/verifier_animation.py b/8x32 PI/outils/verifier_animation.py new file mode 100755 index 0000000..9dbd4fb --- /dev/null +++ b/8x32 PI/outils/verifier_animation.py @@ -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() diff --git a/8x32 PI/schemas/branchement.svg b/8x32 PI/schemas/branchement.svg new file mode 100644 index 0000000..be6ffb7 --- /dev/null +++ b/8x32 PI/schemas/branchement.svg @@ -0,0 +1,188 @@ + + Branchement matrice WS2812B 8×32, Raspberry Pi, 74AHCT125 et alimentation d'établi + + + Matrice WS2812B 8 × 32 — vue de dos (côté fils) + + + + + les données entrent par DIN et traversent les 256 LED + 5V + GND + DOUT + 5V + GND + 5V + GND + DIN + + + + + Sortie DOUT : ne rien brancher + (sert à chaîner un 2e panneau) + + + + + + + − + Condensateur 1000 µF + bande « − » côté GND (fil noir) + patte + côté 5V (fil rouge) + + Alimentation d'établi + + 5.00 V + 3.00 A + + tension + + courant + + + + + − + bornes de + sortie + + + + + + connecteur DIN de la matrice + + fiche fournie (rouge / blanc / vert) + + plaque d'essai + + + + + + + + + + + + + 74AHCT125 + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 + + 10 + + 11 + + 12 + + 13 + + 14 + 1OE + 1A + 1Y + GND + + + + + + 330 Ω + fil rouge de la fiche : il alimente + la puce (patte 14 = VCC, 5 V) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + 1 + 40 + 39 + + + Raspberry Pi + 3B+, 4 ou Zero 2 W (pas le Pi 5) + patte 12 = GPIO 18 + patte 6 = GND + + pattes 2 et 4 (5 V) : NE PAS relier à la matrice + + + bloc secteur USB du Pi + (5 V, 2,5 A ou plus) + + Code couleur des fils + + rouge = + 5 V + + noir = GND (masse) + + blanc = GND (masse) + + vert = données 5 V + + jaune = données 3,3 V + Tous les GND sont reliés : + Pi ↔ puce ↔ fil blanc ↔ matrice + ↔ fil noir ↔ borne − de l'alim. + Alim. réglée sur 5,0 V maxi ! + Un pont = les fils se croisent + sans se toucher. + diff --git a/8x32 PI/schemas/brochage_74ahct125.svg b/8x32 PI/schemas/brochage_74ahct125.svg new file mode 100644 index 0000000..f73f968 --- /dev/null +++ b/8x32 PI/schemas/brochage_74ahct125.svg @@ -0,0 +1,62 @@ + + Brochage du 74AHCT125 (vu de dessus) + + + 74AHCT125 vu de DESSUS (encoche à gauche) + + + + 74AHCT125 + + 1 + 1OE + + → GND + + 2 + 1A + + ← GPIO 18 + + 3 + 1Y + + → 330 Ω → DIN + + 4 + 2OE + + 5 + 2A + + 6 + 2Y + + 7 + GND + + → GND + + 14 + VCC + ← 5 V + + 13 + 4OE + + 12 + 4A + + 11 + 4Y + + 10 + 3OE + + 9 + 3A + + 8 + 3Y + 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). + diff --git a/8x32 PI/schemas/chaine_krita_matrice.svg b/8x32 PI/schemas/chaine_krita_matrice.svg new file mode 100644 index 0000000..69b3ec1 --- /dev/null +++ b/8x32 PI/schemas/chaine_krita_matrice.svg @@ -0,0 +1,55 @@ + + De Krita à la matrice : le trajet d'une animation + + + + + + 1. Dessiner + Krita + document 32 × 8 px + pixel art, sans flou + + + + + 2. Exporter + GIF animé + ou séquence de PNG + (Rendu de l'animation) + + + + + 3. Vérifier + verifier_animation.py + + simulateur + sur l'ordinateur + + + + + 4. Déposer + dossier « animations » + carte SD ou réseau + (01_, 02_… = ordre) + + + + + 5. Lire + Raspberry Pi + lecteur.py : gamma, + luminosité, serpentin + + + + + 6. Afficher + 74AHCT125 (3,3 → 5 V) + puis 256 LED + WS2812B + De Krita à la matrice + + Étapes 1 à 4 : l'équipe design, sur n'importe quel ordinateur. Étapes 5 et 6 : automatiques, dans la vitrine. + diff --git a/8x32 PI/test_matrice.py b/8x32 PI/test_matrice.py new file mode 100644 index 0000000..d0d7edc --- /dev/null +++ b/8x32 PI/test_matrice.py @@ -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() diff --git a/CLAUDE.md b/CLAUDE.md index b4e5066..49bddb1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,13 @@ Le projet avance en deux temps : - Alimenté par une **alimentation d'établi** (5,0 V, limite ≈ 3 A) sur la paire du milieu ; l'Arduino Uno relie seulement données (pin 6 via 330 Ω) et GND (fil blanc). - Code : `matrice_8x32.ino` (FastLED), README pour débutants, schéma `schema_branchement.svg`. +### Variante Raspberry Pi (dossier `8x32 PI/`) +- Une équipe design dessine les animations dans **Krita** (32 × 8 px, export séquence PNG ou GIF) ; le Pi les joue telles quelles. +- Python : `matrice.py` (moteur : réglages, `XY()`, gamma 2.2, limite de courant, écrans `MatriceReelle` / `Simulateur` tkinter / `Enregistreur` GIF), `lecteur.py` (joue en boucle `/boot/firmware/animations` ou `animations/`), `test_matrice.py`, `mes_animations.py` (animations de l'Arduino traduites). +- Matériel : Pi 3B+ / 4 / Zero 2 W (pas le Pi 5 : `rpi_ws281x` ne le gère pas), GPIO 18 (patte 12) → **74AHCT125** (3,3 V → 5 V, VCC pris sur le fil rouge de la fiche DIN) → 330 Ω → DIN. Audio désactivé (`dtparam=audio=off`). +- Installation : `installation/installer.sh` (venv `~/vitrine-env`, service systemd `vitrine-pixels`). Docs : README, INSTALLATION_PI, GUIDE_KRITA, MODE_VITRINE (overlay FS), CODER_EN_PYTHON. +- Schémas générés par `outils/schemas.py`, exemples et palette Krita par `outils/generer_exemples.py`. + ### Chiffres clés - 320 LED adressées (8 × 40) → 960 octets de RAM sur Uno (2 Ko) : serré, envisager Mega ou ESP32. - Rafraîchissement ≈ 30 µs par LED → ≈ 10 ms pour tout le panneau.