From 156f9ce4388c53045cfd5159b207559e598886ca Mon Sep 17 00:00:00 2001
From: bach
Date: Thu, 8 Oct 2026 20:15:01 +0200
Subject: [PATCH] =?UTF-8?q?8x32=20PI=20:=20s=C3=A9quences=20jusqu'=C3=A0?=
=?UTF-8?q?=20120=20PNG,=20test=20du=20fil=20de=20donn=C3=A9es,=20montage?=
=?UTF-8?q?=20A=20valid=C3=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
8x32 PI/INTERFACE_WEB.md | 13 +++--
8x32 PI/README.md | 32 +++++++++--
8x32 PI/outils/test_fil_donnees.py | 86 ++++++++++++++++++++++++++++++
8x32 PI/serveur_web.py | 33 ++++++++++--
8x32 PI/web/page.html | 3 +-
CLAUDE.md | 2 +-
6 files changed, 155 insertions(+), 14 deletions(-)
create mode 100644 8x32 PI/outils/test_fil_donnees.py
diff --git a/8x32 PI/INTERFACE_WEB.md b/8x32 PI/INTERFACE_WEB.md
index f70afa4..5b7ad1d 100644
--- a/8x32 PI/INTERFACE_WEB.md
+++ b/8x32 PI/INTERFACE_WEB.md
@@ -83,7 +83,11 @@ Pour un export Krita en **séquence d'images** (`feu_0000.png`,
3. **Envoyer**.
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
@@ -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 :
protection totale (et la page sert seulement à regarder et à jouer), ou
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).
---
@@ -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 |
| `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`.
diff --git a/8x32 PI/README.md b/8x32 PI/README.md
index 3743197..d77dfe5 100644
--- a/8x32 PI/README.md
+++ b/8x32 PI/README.md
@@ -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
**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
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
@@ -184,7 +185,10 @@ En résumé :
matrice, et ne doit **jamais** toucher une patte du Pi.
- 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
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
@@ -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 :
-il s'arrête au collecteur du transistor. Ce montage est plus lent qu'un
-74AHCT125 : il marche souvent bien, mais c'est un dépannage.
+il s'arrête au collecteur du transistor.
+
+> **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.
@@ -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 |
| `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
> [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 |
| **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 |
-| **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 |
| **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) |
diff --git a/8x32 PI/outils/test_fil_donnees.py b/8x32 PI/outils/test_fil_donnees.py
new file mode 100644
index 0000000..edb705a
--- /dev/null
+++ b/8x32 PI/outils/test_fil_donnees.py
@@ -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()
diff --git a/8x32 PI/serveur_web.py b/8x32 PI/serveur_web.py
index 4c28b7e..23a3b70 100644
--- a/8x32 PI/serveur_web.py
+++ b/8x32 PI/serveur_web.py
@@ -85,8 +85,21 @@ PORT = 80
# d'utilisateur, n'importe lequel, et ce mot de passe).
MOT_DE_PASSE = None
-# Taille maximum d'un envoi, en mégaoctets.
-TAILLE_MAX_MO = 50
+# Taille maximum d'un envoi, en mégaoctets (tous les fichiers ensemble).
+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
# un point : le lecteur les ignore (voir lecteur.est_cache).
@@ -344,6 +357,7 @@ def accueil():
else ":" + request.host.rsplit(":", 1)[1],
extensions=",".join(lecteur.EXTENSIONS),
taille_max=TAILLE_MAX_MO,
+ max_images=MAX_IMAGES_SEQUENCE,
largeur=LARGEUR, hauteur=HAUTEUR,
)
@@ -383,6 +397,12 @@ def ajouter():
fichiers = [f for f in request.files.getlist("fichiers") if f.filename]
if not fichiers:
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:
base, extension = os.path.splitext(fichier.filename)
extension = extension.lower()
@@ -391,7 +411,7 @@ def ajouter():
f"(acceptés : {', '.join(lecteur.EXTENSIONS)}).", "erreur")
continue
# 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)
fichier.save(temporaire)
probleme = verifier_animation(temporaire)
@@ -419,8 +439,13 @@ def ajouter_sequence():
if len(fichiers) < 2:
flash("Une séquence demande au moins 2 images PNG.", "erreur")
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):
if not fichier.filename.lower().endswith(".png"):
shutil.rmtree(temporaire)
diff --git a/8x32 PI/web/page.html b/8x32 PI/web/page.html
index de3f2d4..0b1a1e8 100644
--- a/8x32 PI/web/page.html
+++ b/8x32 PI/web/page.html
@@ -57,7 +57,8 @@