Files
vitrine-pixels/8x32 PI/matrice.py
T
bachandClaude Opus 5.5 2d4b0d5a5f 8x32 PI : interface web, matrice verticale, compatibilité Raspbian 11
- Interface web (serveur_web.py, Flask, gabarits dans web/) : dépôt de GIF,
  PNG animés et séquences de PNG, aperçus animés, ordre de lecture,
  activer / désactiver, corbeille, « jouer maintenant » et « suivante ».
  Service systemd vitrine-web, activé par installer.sh.
- Lecteur : réagit aux demandes de la page web (post-it dans
  /run/vitrine-pixels), écrit l'animation en cours, éteint la matrice
  proprement sur SIGTERM.
- Matrice posée debout : réglage ROTATION (90 = image 8 × 32) dans
  matrice.py ; images couchées tournées automatiquement ; exemples,
  animations codées, aperçus et page web adaptés au format vertical.
- Compatibilité Raspbian 11 Bullseye : partition /boot, Pillow 8.1, Flask 1.1.
- Doc : nouveau guide INTERFACE_WEB.md ; README, GUIDE_KRITA, MODE_VITRINE,
  CODER_EN_PYTHON mis à jour (format 8 × 32, adresses Wi-Fi / câble du Pi).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:08:18 +02:00

543 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
=============================================================================
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
# =============================================================================
# Le PANNEAU, tel qu'il est fabriqué : 32 colonnes × 8 lignes de LED,
# câblées en serpentin par colonnes de 8. Ça ne change jamais.
COLONNES_PANNEAU = 32
LIGNES_PANNEAU = 8
NB_LEDS = COLONNES_PANNEAU * LIGNES_PANNEAU # = 256
# ROTATION : comment le panneau est POSÉ dans la vitrine, en degrés.
# 0 = couché (à l'horizontale) : l'image fait 32 de large × 8 de haut ;
# 90 = debout (à la verticale) : l'image fait 8 de large × 32 de haut.
# (180 et 270 existent aussi : couché à l'envers, debout dans l'autre sens.
# Pour remettre l'image à l'endroit, INVERSER_X / INVERSER_Y suffisent.)
ROTATION = 90
# Taille de l'IMAGE à dessiner (dans Krita, dans le code…), selon la rotation.
# Tout le reste du programme utilise LARGEUR et HAUTEUR.
if ROTATION in (90, 270):
LARGEUR, HAUTEUR = LIGNES_PANNEAU, COLONNES_PANNEAU # 8 × 32 : debout
else:
LARGEUR, HAUTEUR = COLONNES_PANNEAU, LIGNES_PANNEAU # 32 × 8 : couché
# 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
# SENS de l'image (voir test_matrice.py), une fois ROTATION choisie.
# 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
# - les deux à la fois (image à l'envers) → les deux à 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, sur l'IMAGE telle qu'on la voit :
# x = colonne, de 0 (à gauche) à LARGEUR - 1 (à droite)
# y = ligne, de 0 (en haut) à HAUTEUR - 1 (en bas)
# Le point (0, 0) est en haut à gauche, comme dans Krita.
#
# Pour trouver la LED correspondante, trois étapes :
#
# 1. Retourner l'image si besoin (INVERSER_X / INVERSER_Y).
#
# 2. Tourner : passer de la position sur l'IMAGE à la position sur le
# PANNEAU (px de 0 à 31, py de 0 à 7, panneau vu couché). Exemple,
# panneau debout (ROTATION = 90, tourné d'un quart de tour vers la
# droite) : le haut de l'image correspond à la gauche du panneau, et la
# droite de l'image au haut du panneau.
#
# 3. Le serpentin : les LED sont numérotées dans l'ordre de la chaîne, qui
# descend la 1re colonne du panneau, remonte la 2e…
#
# px=0 px=1 px=2 px=3
# py=0 0 15 16 31 …
# py=1 1 14 17 30
# … … … … …
# py=7 7 → 8 23 → 24
#
# L'étape 3 est la même fonction que dans matrice_8x32.ino.
def XY(x, y):
"""Renvoie le numéro (0 à 255) de la LED qui affiche le pixel (x, y) de l'image."""
# 1. Retourner.
if INVERSER_X:
x = (LARGEUR - 1) - x
if INVERSER_Y:
y = (HAUTEUR - 1) - y
# 2. Tourner : position (px, py) sur le panneau couché.
if ROTATION == 90:
px, py = y, (LIGNES_PANNEAU - 1) - x
elif ROTATION == 180:
px, py = (COLONNES_PANNEAU - 1) - x, (LIGNES_PANNEAU - 1) - y
elif ROTATION == 270:
px, py = (COLONNES_PANNEAU - 1) - y, x
else:
px, py = x, y
# 3. Le serpentin.
if px % 2 == 0:
# Colonne paire du panneau : la chaîne DESCEND.
return px * LIGNES_PANNEAU + py
else:
# Colonne impaire : la chaîne REMONTE.
return px * LIGNES_PANNEAU + (LIGNES_PANNEAU - 1 - py)
# =============================================================================
# 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 d'un pixel à l'écran, en points : plus petit si l'image est
# haute (panneau debout), pour que la fenêtre tienne dans l'écran.
TAILLE = min(26, 640 // max(LARGEUR, HAUTEUR))
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()