8x32 PI : séquences jusqu'à 120 PNG, test du fil de données, montage A validé

- serveur_web.py : séquences limitées à 120 PNG (message clair au-delà),
  envois jusqu'à 200 Mo, fichiers temporaires sur la carte SD (et plus dans
  /run, trop petit), garde-fou si une séquence est envoyée dans le cadre
  « fichiers séparés ».
- outils/test_fil_donnees.py : met GPIO 18 à 1 puis à 0 toutes les 10 s,
  pour suivre le signal au multimètre du Pi jusqu'à DIN.
- README : montage A (Pi en direct, LED en 4,4 V) testé et validé ; montage B
  (transistor 2N3904) déconseillé, trop lent (temps de stockage ≈ 0,2 µs
  face à un « 0 » de 0,4 µs) ; mise en garde sur les convertisseurs HV/LV ;
  dépannage « seule la 1re LED s'allume ».
- INTERFACE_WEB.md, CLAUDE.md : nouvelles limites, accès au Pi en Wi-Fi seul,
  lecteur activé au démarrage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bach
2026-10-08 20:15:01 +02:00
co-authored by Claude Opus 5.5
parent 1ea8500b82
commit 156f9ce438
6 changed files with 155 additions and 14 deletions
+10 -3
View File
@@ -83,7 +83,11 @@ Pour un export Krita en **séquence d'images** (`feu_0000.png`,
3. **Envoyer**. 3. **Envoyer**.
Les images sont mises dans l'ordre de leurs noms : gardez la numérotation de Les images sont mises dans l'ordre de leurs noms : gardez la numérotation de
Krita. Krita. **120 images au maximum** par séquence.
> ⚠️ N'envoyez pas une séquence dans le cadre « Un ou plusieurs fichiers » :
> chaque PNG deviendrait une animation séparée. Au-delà de 10 PNG envoyés
> d'un coup, la page refuse et vous redirige vers le bon cadre.
### Vérifications automatiques ### Vérifications automatiques
@@ -138,7 +142,9 @@ qu'on ne clique pas sur **Vider**. Vider la corbeille efface **définitivement**
rien modifier et affiche une erreur. Pendant l'exposition, on peut choisir : rien modifier et affiche une erreur. Pendant l'exposition, on peut choisir :
protection totale (et la page sert seulement à regarder et à jouer), ou protection totale (et la page sert seulement à regarder et à jouer), ou
partition de démarrage modifiable (et la page reste pleinement utilisable). partition de démarrage modifiable (et la page reste pleinement utilisable).
- **Taille maximum** d'un envoi : 50 Mo (largement assez : un GIF de 8 × 32 - **Une séquence de PNG** compte **120 images au maximum** (10 secondes à
12 images/s). Plus long : découpez-la, ou exportez-la en GIF.
- **Taille maximum** d'un envoi : 200 Mo (largement assez : un GIF de 8 × 32
pèse quelques kilo-octets). pèse quelques kilo-octets).
--- ---
@@ -198,7 +204,8 @@ Les réglages sont en haut de [`serveur_web.py`](serveur_web.py) :
|---|---|---| |---|---|---|
| `MOT_DE_PASSE` | `None` | ex. `"pixels"` : le navigateur demande un nom (n'importe lequel) et ce mot de passe | | `MOT_DE_PASSE` | `None` | ex. `"pixels"` : le navigateur demande un nom (n'importe lequel) et ce mot de passe |
| `PORT` | `80` | le port web ; avec un autre, l'adresse devient `http://172.16.50.27:8080` | | `PORT` | `80` | le port web ; avec un autre, l'adresse devient `http://172.16.50.27:8080` |
| `TAILLE_MAX_MO` | `50` | taille maximum d'un envoi | | `TAILLE_MAX_MO` | `200` | taille maximum d'un envoi |
| `MAX_IMAGES_SEQUENCE` | `120` | nombre maximum d'images d'une séquence de PNG |
Après une modification : `sudo systemctl restart vitrine-web`. Après une modification : `sudo systemctl restart vitrine-web`.
+27 -5
View File
@@ -160,6 +160,7 @@ inverse des aiguilles d'une montre : 1 à 7 en bas, de gauche à droite, puis
### Pas de 74AHCT125 sous la main ? Deux solutions de dépannage ### Pas de 74AHCT125 sous la main ? Deux solutions de dépannage
**Solution A : baisser la tension de la matrice (conseillée, aucun composant).** **Solution A : baisser la tension de la matrice (conseillée, aucun composant).**
✅ *Testée et validée sur le Pi 3 du workshop (octobre 2026).*
Le seuil du « 1 » vaut 70 % de la tension d'alimentation **des LED**. Si on Le seuil du « 1 » vaut 70 % de la tension d'alimentation **des LED**. Si on
alimente la matrice en **4,4 V** au lieu de 5 V, le seuil tombe à alimente la matrice en **4,4 V** au lieu de 5 V, le seuil tombe à
0,7 × 4,4 ≈ **3,1 V**, et les 3,3 V du Pi suffisent. Les WS2812B fonctionnent 0,7 × 4,4 ≈ **3,1 V**, et les 3,3 V du Pi suffisent. Les WS2812B fonctionnent
@@ -184,7 +185,10 @@ En résumé :
matrice, et ne doit **jamais** toucher une patte du Pi. matrice, et ne doit **jamais** toucher une patte du Pi.
- Gardez le fil de données **court** (moins de 30 cm). - Gardez le fil de données **court** (moins de 30 cm).
**Solution B : un transistor « inverseur » (si la solution A scintille).** **Solution B : un transistor « inverseur » (déconseillée).**
❌ *Essayée pendant le workshop : la matrice ne s'allume pas, seule la 1re LED
prend une couleur au hasard.* On la décrit quand même, pour comprendre
pourquoi elle échoue.
Avec un transistor NPN du kit, de préférence un **2N3904** (le plus rapide du Avec un transistor NPN du kit, de préférence un **2N3904** (le plus rapide du
kit ; un BC337 ou un S8050 peuvent convenir), on fabrique un adaptateur 5 V. kit ; un BC337 ou un S8050 peuvent convenir), on fabrique un adaptateur 5 V.
Le transistor **inverse** le signal (un 1 devient un 0) : il faut le dire au Le transistor **inverse** le signal (un 1 devient un 0) : il faut le dire au
@@ -209,8 +213,25 @@ Le même montage, en schéma électrique :
``` ```
Ici l'alimentation d'établi reste à **5,0 V**. Le 5 V n'atteint jamais le Pi : Ici l'alimentation d'établi reste à **5,0 V**. Le 5 V n'atteint jamais le Pi :
il s'arrête au collecteur du transistor. Ce montage est plus lent qu'un il s'arrête au collecteur du transistor.
74AHCT125 : il marche souvent bien, mais c'est un dépannage.
> **Pourquoi ça ne marche pas : le transistor est trop lent.** Les LED lisent
> des impulsions très courtes : un « 0 » est une impulsion de **0,4 µs**
> (0,000 000 4 s), un « 1 » une impulsion de 0,8 µs. Or un transistor comme
> le 2N3904, quand il conduit à fond (« saturé »), met environ **0,2 µs** à se
> bloquer : c'est son *temps de stockage*. Chaque impulsion est raccourcie
> d'autant, et la résistance de tirage arrondit encore les fronts. Le « 0 »
> tombe sous ce que la LED accepte : elle reçoit du bruit, la première LED
> affiche une couleur au hasard et ne transmet rien aux suivantes.
> Un 74AHCT125 réagit en moins de 0,01 µs : 20 fois plus vite.
>
> On pourrait l'accélérer (condensateur de 100 pF en parallèle de la
> résistance de base, résistance de tirage plus faible), mais le résultat
> resterait fragile. **Préférez la solution A, ou la puce.**
> **Même problème avec les petits modules « convertisseurs de niveau »**
> bidirectionnels (marqués HV / LV, à transistors BSS138) : ils sont faits
> pour des signaux lents (I²C), pas pour les 800 000 bits par seconde des LED.
> Dans les deux cas, le reste (logiciel, installation, Krita) ne change pas. > Dans les deux cas, le reste (logiciel, installation, Krita) ne change pas.
@@ -532,7 +553,8 @@ En haut de [`serveur_web.py`](serveur_web.py) :
|---|---|---| |---|---|---|
| `MOT_DE_PASSE` | `None` | ⚠️ sans mot de passe, **toute personne sur le réseau** peut ajouter ou supprimer des animations. Ex. `"pixels"` : le navigateur le demandera | | `MOT_DE_PASSE` | `None` | ⚠️ sans mot de passe, **toute personne sur le réseau** peut ajouter ou supprimer des animations. Ex. `"pixels"` : le navigateur le demandera |
| `PORT` | `80` | le port web habituel ; un autre port s'ajoute à l'adresse (`http://172.16.50.27:8080`) | | `PORT` | `80` | le port web habituel ; un autre port s'ajoute à l'adresse (`http://172.16.50.27:8080`) |
| `TAILLE_MAX_MO` | `50` | taille maximum d'un envoi | | `TAILLE_MAX_MO` | `200` | taille maximum d'un envoi (tous les fichiers ensemble) |
| `MAX_IMAGES_SEQUENCE` | `120` | nombre maximum d'images dans une séquence de PNG |
> **Avec la protection contre les coupures de courant** (overlay, voir > **Avec la protection contre les coupures de courant** (overlay, voir
> [MODE_VITRINE.md](MODE_VITRINE.md#4-survivre-aux-coupures-de-courant)), si la > [MODE_VITRINE.md](MODE_VITRINE.md#4-survivre-aux-coupures-de-courant)), si la
@@ -563,7 +585,7 @@ Le mode d'emploi détaillé, pour l'équipe design, est dans
| **Ç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 | | **Ç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 | | **Couleurs aléatoires, scintillement** | Puce absente ou de type HC (sans T) | Vérifier les lettres sur la puce |
| | Montage sans puce (solution A) | Baisser encore un peu l'alimentation (4,2 V), raccourcir le fil vert, ou passer à la solution B | | | Montage sans puce (solution A) | Baisser encore un peu l'alimentation (4,2 V), raccourcir le fil vert, ou passer à la solution B |
| **Rien ne s'allume avec le transistor** (solution B) | `INVERSER_SIGNAL` oublié, ou transistor à l'envers | `INVERSER_SIGNAL = True` dans `matrice.py` ; vérifier E-B-C | | **Seule la 1re LED s'allume, d'une couleur au hasard** | Signal déformé : convertisseur ou transistor trop lent (solution B), masse non commune | Passer à la solution A (sans convertisseur, LED en 4,4 V), vérifier le fil blanc |
| | Masse mal serrée | Revérifier toutes les masses | | | 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` | | **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) | | **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) |
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
=============================================================================
TEST DU FIL DE DONNÉES AU MULTIMÈTRE
Workshop « Vitrine pixels »
=============================================================================
Quand la matrice ne s'allume pas du tout, il faut savoir OÙ le signal se
perd : à la sortie du Pi ? dans le convertisseur de niveau ? dans un fil ?
Le vrai signal des LED est beaucoup trop rapide pour un multimètre
(800 000 bits par seconde). Ce programme transforme donc la patte 12
(GPIO 18) en simple interrupteur LENT :
10 secondes à « 1 » (3,3 V à la sortie du Pi)
10 secondes à « 0 » (0 V)
… et on recommence, pendant 2 minutes (Ctrl + C pour arrêter).
Pendant ce temps, on mesure au multimètre (calibre 20 V continu, pointe
NOIRE sur un GND, pointe ROUGE sur le point à tester) :
Point mesuré pendant « 1 » pendant « 0 »
------------------------------------------- ------------- -------------
1. patte 12 du Pi (GPIO 18) ≈ 3,3 V 0 V
2. entrée du convertisseur (côté 3,3 V) ≈ 3,3 V 0 V
3. sortie du convertisseur (côté 5 V) ≈ 5 V 0 V (*)
4. après la résistance de 330 Ω ≈ 5 V 0 V (*)
5. fil VERT de la fiche, au plus près de DIN ≈ 5 V 0 V (*)
(*) Avec le montage A (sans convertisseur, matrice en 4,4 V) : 3,3 V.
Avec le montage B (transistor) : INVERSÉ, ≈ 5 V pendant « 0 » et
0 V pendant « 1 ». C'est normal.
Le premier point où la mesure est fausse indique la panne : le problème se
trouve entre ce point et le précédent.
Et aussi, sans rien lancer :
- entre le 5V et le GND des fils du MILIEU de la matrice : 5,0 V (ou 4,4 V) ;
- entre le GND du Pi et le GND de la matrice : 0 V (sinon, masse pas commune).
Lancer (le lecteur ne doit pas tourner en même temps) :
sudo systemctl stop vitrine-pixels
sudo ~/vitrine-env/bin/python outils/test_fil_donnees.py
Le prochain programme qui affiche sur la matrice remettra tout seul la
patte 12 dans son mode normal.
"""
import shutil
import subprocess
import sys
import time
BROCHE = "18"
DUREE_ETAT = 10 # secondes à « 1 », puis à « 0 »
DUREE_TOTALE = 120 # secondes
def regler(niveau):
"""Met la patte en sortie, à 1 (dh = « drive high ») ou à 0 (dl)."""
# « raspi-gpio » sur Raspbian 11, « pinctrl » sur les systèmes plus récents.
outil = "raspi-gpio" if shutil.which("raspi-gpio") else "pinctrl"
subprocess.run([outil, "set", BROCHE, "op", "dh" if niveau else "dl"], check=True)
def main():
print(__doc__.split("Lancer")[0])
debut = time.monotonic()
niveau = 1
try:
while time.monotonic() - debut < DUREE_TOTALE:
regler(niveau)
print(f"{time.strftime('%H:%M:%S')} patte 12 à « {niveau} » "
f"({'≈ 3,3 V' if niveau else '0 V'} à la sortie du Pi)")
sys.stdout.flush()
time.sleep(DUREE_ETAT)
niveau = 1 - niveau
except KeyboardInterrupt:
pass
finally:
regler(0)
print("Terminé : patte 12 à 0.")
if __name__ == "__main__":
main()
+29 -4
View File
@@ -85,8 +85,21 @@ PORT = 80
# d'utilisateur, n'importe lequel, et ce mot de passe). # d'utilisateur, n'importe lequel, et ce mot de passe).
MOT_DE_PASSE = None MOT_DE_PASSE = None
# Taille maximum d'un envoi, en mégaoctets. # Taille maximum d'un envoi, en mégaoctets (tous les fichiers ensemble).
TAILLE_MAX_MO = 50 TAILLE_MAX_MO = 200
# Nombre maximum d'images dans une SÉQUENCE de PNG (120 images = 10 secondes
# à 12 images/s, 5 secondes à 24 images/s).
MAX_IMAGES_SEQUENCE = 120
# Au-delà de ce nombre de PNG envoyés d'un coup dans le cadre « Un ou
# plusieurs fichiers », c'est sûrement une séquence envoyée au mauvais
# endroit (chaque PNG deviendrait une animation) : on refuse et on explique.
MAX_PNG_SEPARES = 10
# Où ranger les envois le temps de les vérifier : le dossier temporaire du
# système (sur la carte SD), pas /run qui est en mémoire vive et trop petit.
DOSSIER_ENVOIS = tempfile.gettempdir()
# Sous-dossiers spéciaux du dossier des animations. Leur nom commence par # Sous-dossiers spéciaux du dossier des animations. Leur nom commence par
# un point : le lecteur les ignore (voir lecteur.est_cache). # un point : le lecteur les ignore (voir lecteur.est_cache).
@@ -344,6 +357,7 @@ def accueil():
else ":" + request.host.rsplit(":", 1)[1], else ":" + request.host.rsplit(":", 1)[1],
extensions=",".join(lecteur.EXTENSIONS), extensions=",".join(lecteur.EXTENSIONS),
taille_max=TAILLE_MAX_MO, taille_max=TAILLE_MAX_MO,
max_images=MAX_IMAGES_SEQUENCE,
largeur=LARGEUR, hauteur=HAUTEUR, largeur=LARGEUR, hauteur=HAUTEUR,
) )
@@ -383,6 +397,12 @@ def ajouter():
fichiers = [f for f in request.files.getlist("fichiers") if f.filename] fichiers = [f for f in request.files.getlist("fichiers") if f.filename]
if not fichiers: if not fichiers:
flash("Aucun fichier choisi.", "erreur") flash("Aucun fichier choisi.", "erreur")
nb_png = sum(f.filename.lower().endswith(".png") for f in fichiers)
if nb_png > MAX_PNG_SEPARES:
flash(f"{nb_png} PNG envoyés d'un coup : est-ce une séquence d'images ? "
"Utilisez plutôt le cadre « Une séquence de PNG », sinon chaque image "
"deviendrait une animation séparée.", "erreur")
return redirect(url_for("accueil"))
for fichier in fichiers: for fichier in fichiers:
base, extension = os.path.splitext(fichier.filename) base, extension = os.path.splitext(fichier.filename)
extension = extension.lower() extension = extension.lower()
@@ -391,7 +411,7 @@ def ajouter():
f"(acceptés : {', '.join(lecteur.EXTENSIONS)}).", "erreur") f"(acceptés : {', '.join(lecteur.EXTENSIONS)}).", "erreur")
continue continue
# On enregistre d'abord dans un dossier temporaire, on vérifie, puis on range. # On enregistre d'abord dans un dossier temporaire, on vérifie, puis on range.
descripteur, temporaire = tempfile.mkstemp(suffix=extension, dir=lecteur.DOSSIER_ETAT) descripteur, temporaire = tempfile.mkstemp(suffix=extension, dir=DOSSIER_ENVOIS)
os.close(descripteur) os.close(descripteur)
fichier.save(temporaire) fichier.save(temporaire)
probleme = verifier_animation(temporaire) probleme = verifier_animation(temporaire)
@@ -419,8 +439,13 @@ def ajouter_sequence():
if len(fichiers) < 2: if len(fichiers) < 2:
flash("Une séquence demande au moins 2 images PNG.", "erreur") flash("Une séquence demande au moins 2 images PNG.", "erreur")
return redirect(url_for("accueil")) return redirect(url_for("accueil"))
if len(fichiers) > MAX_IMAGES_SEQUENCE:
flash(f"{len(fichiers)} images : c'est trop, une séquence peut en compter "
f"{MAX_IMAGES_SEQUENCE} au maximum. Découpez-la en plusieurs "
"animations, ou exportez-la en GIF.", "erreur")
return redirect(url_for("accueil"))
temporaire = tempfile.mkdtemp(dir=lecteur.DOSSIER_ETAT) temporaire = tempfile.mkdtemp(dir=DOSSIER_ENVOIS)
for i, fichier in enumerate(fichiers): for i, fichier in enumerate(fichiers):
if not fichier.filename.lower().endswith(".png"): if not fichier.filename.lower().endswith(".png"):
shutil.rmtree(temporaire) shutil.rmtree(temporaire)
+2 -1
View File
@@ -57,7 +57,8 @@
<form class="carte" method="post" action="{{ url_for('ajouter_sequence') }}" enctype="multipart/form-data"> <form class="carte" method="post" action="{{ url_for('ajouter_sequence') }}" enctype="multipart/form-data">
<h3>Une séquence de PNG</h3> <h3>Une séquence de PNG</h3>
<p class="aide">Toutes les images d'un export Krita « séquence d'images » <p class="aide">Toutes les images d'un export Krita « séquence d'images »
(<code>feu_0000.png</code>, <code>feu_0001.png</code>…), pour une seule animation.</p> (<code>feu_0000.png</code>, <code>feu_0001.png</code>…), pour une seule animation :
<strong>{{ max_images }} images au maximum</strong>.</p>
<label class="depot"> <label class="depot">
<input type="file" name="images" multiple accept=".png,image/png"> <input type="file" name="images" multiple accept=".png,image/png">
<span>Choisir les PNG…</span> <span>Choisir les PNG…</span>
+1 -1
View File
@@ -80,7 +80,7 @@ Le projet avance en deux temps :
- 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`). - 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`, services systemd `vitrine-pixels` (lecteur) et `vitrine-web` (interface)). Docs : README, INSTALLATION_PI, GUIDE_KRITA, MODE_VITRINE (overlay FS), INTERFACE_WEB, CODER_EN_PYTHON. - Installation : `installation/installer.sh` (venv `~/vitrine-env`, services systemd `vitrine-pixels` (lecteur) et `vitrine-web` (interface)). Docs : README, INSTALLATION_PI, GUIDE_KRITA, MODE_VITRINE (overlay FS), INTERFACE_WEB, CODER_EN_PYTHON.
- Interface web `serveur_web.py` (Flask, port 80, gabarits dans `web/`) : dépôt GIF/PNG/séquences, ordre (renommage `01_…`), activer/désactiver (`.desactivees/`), corbeille (`.corbeille/`), « jouer maintenant » via post-it `/run/vitrine-pixels/demande` lu par le lecteur. - Interface web `serveur_web.py` (Flask, port 80, gabarits dans `web/`) : dépôt GIF/PNG/séquences, ordre (renommage `01_…`), activer/désactiver (`.desactivees/`), corbeille (`.corbeille/`), « jouer maintenant » via post-it `/run/vitrine-pixels/demande` lu par le lecteur.
- Pi installé : Raspberry Pi 3 Model B, Raspbian 11 Bullseye 32 bits (dossier boot = `/boot`, Pillow 8.1, Flask 1.1), `ssh devel@172.16.7.199` (clé ESADHaR, câble eth0) ; Wi-Fi wlan0 = 172.16.50.27. Le Wi-Fi et le filaire de l'école ne se voient pas : depuis le Wi-Fi, page web sur http://172.16.50.27 (adresses DHCP, affichées en bas de la page). Matrice posée debout : `ROTATION = 90`, image 8 × 32. Pas de 74AHCT125 pendant le workshop : montage A (matrice en 4,4 V) ou B (transistor, `INVERSER_SIGNAL`). - Pi installé : Raspberry Pi 3 Model B, Raspbian 11 Bullseye 32 bits (dossier boot = `/boot`, Pillow 8.1, Flask 1.1), Ethernet débranché : accès uniquement en Wi-Fi, `ssh devel@172.16.50.27` (clé ESADHaR) ; l'ancienne adresse câble était 172.16.7.199. Le Wi-Fi et le filaire de l'école ne se voient pas : depuis le Wi-Fi, page web sur http://172.16.50.27 (adresses DHCP, affichées en bas de la page). Matrice posée debout : `ROTATION = 90`, image 8 × 32. Pas de 74AHCT125 pendant le workshop : **montage A** (GPIO 18 → 330 Ω → DIN, matrice en 4,4 V) testé et validé ; montage B (transistor 2N3904 inverseur) essayé : échec, trop lent (temps de stockage ≈ 0,2 µs face à un « 0 » de 0,4 µs), seule la 1re LED s'allume au hasard. Lecteur `vitrine-pixels` activé au démarrage le 2026-10-08.
- Schémas générés par `outils/schemas.py`, exemples et palette Krita par `outils/generer_exemples.py`. - Schémas générés par `outils/schemas.py`, exemples et palette Krita par `outils/generer_exemples.py`.
### Chiffres clés ### Chiffres clés