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 @@