E-paper : un écran qui garde l’image sans courant
Installe la bibliothèque, écris du texte en gros, dessine des formes, repars avec ton badge nominatif et écris sur l’écran depuis ton téléphone : l’image reste affichée même débranché. Rien à câbler, l’écran est déjà sur la carte.
2h
Ce que tu vas créer
Un badge nominatif sur un écran e-paper, et sept petites manipulations pour y arriver : vérifier la bibliothèque, afficher du texte, l’agrandir, dessiner des formes, réagir à un bouton, puis écrire sur l’écran depuis ton téléphone.
L’e-paper fonctionne comme une liseuse : l’image reste affichée sans aucune alimentation. En contrepartie, le rafraîchissement est lent, environ 2 secondes.
Matériel nécessaire
| Composant | Quantité | Notes |
|---|---|---|
| Carte LilyGo T5 V2.3 | 1 | ESP32 + écran e-paper 2,13” (250 × 128 pixels) déjà câblé |
| Câble USB | 1 | De données (pas un câble de charge seule) |
| Ordinateur avec Thonny | 1 | MicroPython déjà installé sur la carte |
Connexions
Aucune. L’écran est soudé sur la carte. Pour information, voici les broches utilisées en interne, elles ne sont donc plus disponibles pour autre chose :
SCK → GPIO18 MOSI → GPIO23
CS → GPIO5 DC → GPIO17
RST → GPIO16 BUSY → GPIO4Déroulé (2 h)
| Durée | Étape |
|---|---|
| 10 min | Présentation : qu’est-ce que l’e-paper, pourquoi ça consomme zéro |
| 15 min | Manip 01 — installer la bibliothèque |
| 10 min | Manip 02 — premier affichage |
| 15 min | Manip 03 — agrandir le texte |
| 10 min | Manip 04 — formes et motifs |
| 20 min | Manip 05 — ton badge nominatif |
| 15 min | Manip 06 — compteur avec le bouton |
| 20 min | Manip 07 — écris sur l’écran depuis ton téléphone |
| 5 min | Questions et rangement (bonus : « Payzac et son FabLab », ESP-NOW) |
Manip 01 — Installer la bibliothèque
Pour piloter l’écran, la carte a besoin d’un fichier de plus : gdeh0213b73.py. C’est le « pilote » de l’écran : il sait lui parler en SPI.
- Télécharge le fichier : gdeh0213b73.py sur GitHub (bouton « Download raw file »).
- Ouvre Thonny et affiche le panneau des fichiers : Affichage → Fichiers.
- Repère le fichier dans la partie haute (ton ordinateur), clic droit → Téléverser vers /.
- Il apparaît dans la partie basse (l’ESP32). C’est installé.
Puis lance ce programme pour vérifier :
manip01_verifier_installation.py .python atelier-epaper/manip01_verifier_installation.py 22 lignes
GitHub
# Atelier 02 — E-paper — Manip 01 : vérifier que la bibliothèque est installée
# Fablab Ardèche — MicroPython
#
# Aucun affichage ici : on vérifie seulement que le fichier gdeh0213b73.py
# est bien sur l'ESP32 et que Python arrive à le lire.
#
# Matériel : carte LilyGo T5 V2.3 (l'écran e-paper est déjà câblé dessus)
# Rien à brancher, à part le câble USB.
import os
print("Fichiers présents sur l'ESP32 :")
print(os.listdir())
try:
import gdeh0213b73
print("OK : la bibliothèque est installée.")
print("Fonctions disponibles :", [n for n in dir(gdeh0213b73) if not n.startswith("_")][:8])
except ImportError:
print("MANQUE : gdeh0213b73.py n'est pas sur l'ESP32.")
print("Dans Thonny : Affichage > Fichiers, clic droit sur le fichier > Téléverser vers /")
Si Thonny affiche ImportError: no module named 'gdeh0213b73', le fichier est resté sur ton ordinateur. Vérifie qu’il apparaît dans la partie basse du panneau des fichiers (celle de l’ESP32), sinon recommence le téléversement.
Autre réflexe : après avoir téléversé, fais Ctrl+D dans le Shell pour redémarrer MicroPython.
Le détail de la bibliothèque (toutes les fonctions, les pièges connus) est dans la fiche gdeh0213b73.
Manip 02 — Premier affichage
manip02_premier_affichage.py .python atelier-epaper/manip02_premier_affichage.py 27 lignes
GitHub
# Atelier 02 — E-paper — Manip 02 : premier affichage
# Fablab Ardèche — MicroPython
#
# On écrit « Bonjour » sur l'écran, puis on mesure combien de temps
# prend le rafraîchissement.
#
# Convention des couleurs : 0 = blanc, 1 = noir.
#
# Matériel : carte LilyGo T5 V2.3 (écran e-paper déjà câblé)
from gdeh0213b73 import init_epd, ROTATION_0
import time
epd = init_epd(rotation=ROTATION_0) # portrait : 128 pixels de large, 250 de haut
epd.fill(0) # tout l'écran en blanc
epd.text("Bonjour", 10, 10, 1) # texte noir, en haut à gauche
epd.text("Fablab Polinno", 10, 30, 1)
epd.rect(0, 0, epd.width, epd.height, 1) # un cadre autour de l'écran
debut = time.ticks_ms()
epd.update() # rien n'apparaît avant cette ligne !
duree = time.ticks_diff(time.ticks_ms(), debut)
print("Rafraîchissement : {} ms".format(duree))
epd.deep_sleep() # l'image reste, l'écran ne consomme plus rien
🤔 Avant de lancer Tu supprimes la ligne `epd.update()`. Que voit-on sur l'écran ? voir la réponse
Rien de nouveau. Tout ce que tu écris avec fill, text, rect est dessiné en mémoire, dans l’ESP32. C’est epd.update() qui envoie l’image vers l’écran, et c’est à ce moment-là seulement que les micro-billes bougent.
C’est le même principe que np.write() pour les NeoPixel : on prépare, puis on envoie d’un coup.
- Remplace
ROTATION_0parROTATION_90: l’écran passe en paysage (250 × 128) - Écris ton prénom à la place de « Bonjour »
- Regarde la durée affichée dans le Shell : environ combien de secondes ?
- Débranche le câble USB : l’image reste-t-elle ?
La police intégrée est en ASCII pur : « é », « è », « à » s’affichent mal. Écris « Ardeche » et non « Ardèche ».
Manip 03 — Agrandir le texte
La police intégrée fait 8 pixels de haut. Pour un badge ou une affiche, c’est trop petit : big_text() redessine chaque pixel sous forme d’un carré plus grand.
manip03_gros_texte.py .python atelier-epaper/manip03_gros_texte.py 46 lignes
GitHub
# Atelier 02 — E-paper — Manip 03 : agrandir le texte
# Fablab Ardèche — MicroPython
#
# La police intégrée fait 8 pixels de haut, c'est petit. big_text() écrit le
# texte dans un mini-tampon, puis redessine chaque pixel sous forme d'un carré
# de « scale × scale » pixels.
#
# Attention : la police est en ASCII pur. Pas d'accents (é, è, à...).
#
# Matériel : carte LilyGo T5 V2.3 (écran e-paper déjà câblé)
from gdeh0213b73 import init_epd, ROTATION_90
import framebuf
def big_text(epd, s, x, y, scale=2, c=1):
"""Texte agrandi : scale=1 → 8 px de haut, scale=2 → 16, scale=3 → 24..."""
w = len(s) * 8
buf = bytearray(((w + 7) // 8) * 8)
fb = framebuf.FrameBuffer(buf, w, 8, framebuf.MONO_HLSB)
fb.fill(0)
fb.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if fb.pixel(tx, ty):
epd.fill_rect(x + tx * scale, y + ty * scale, scale, scale, c)
def texte_centre(epd, s, y, scale=2, c=1):
"""Comme big_text, mais calcule x pour centrer le texte."""
largeur = len(s) * 8 * scale
x = (epd.width - largeur) // 2
big_text(epd, s, x, y, scale, c)
epd = init_epd(rotation=ROTATION_90) # paysage : 250 de large, 128 de haut
epd.fill(0)
epd.text("8 px : texte normal", 4, 4, 1)
big_text(epd, "16 px", 4, 18, scale=2)
big_text(epd, "24 px", 4, 40, scale=3)
texte_centre(epd, "32 px", 76, scale=4)
epd.update()
epd.deep_sleep()
- Change
scale=4enscale=2: comment évolue la taille du texte ? - Écris « ESP32 » à la place de « 32 px »
texte_centrecalculextoute seule : essaie un texte plus long, puis un texte plus court- Inverse les couleurs :
epd.fill(1)au départ, puisc=0dans les appels
Manip 04 — Formes et motifs
manip04_formes.py .python atelier-epaper/manip04_formes.py 39 lignes
GitHub
# Atelier 02 — E-paper — Manip 04 : lignes, rectangles, motifs
# Fablab Ardèche — MicroPython
#
# Les outils de dessin de MicroPython (module framebuf) :
# rect(x, y, l, h, c) rectangle vide
# fill_rect(x, y, l, h, c) rectangle plein
# line(x1, y1, x2, y2, c) ligne quelconque
# hline / vline lignes horizontales / verticales
#
# Origine (0, 0) = coin haut-gauche. En paysage : x de 0 à 249, y de 0 à 127.
#
# Matériel : carte LilyGo T5 V2.3 (écran e-paper déjà câblé)
from gdeh0213b73 import init_epd, ROTATION_90
epd = init_epd(rotation=ROTATION_90)
epd.fill(0)
# --- Un cadre à double trait ---
epd.rect(0, 0, epd.width, epd.height, 1)
epd.rect(3, 3, epd.width - 6, epd.height - 6, 1)
# --- Une cible : des rectangles emboîtés ---
for i in range(0, 40, 8):
epd.rect(15 + i, 20 + i, 90 - 2 * i, 90 - 2 * i, 1)
# --- Un damier de 4 x 4 cases ---
for ligne in range(4):
for col in range(4):
if (ligne + col) % 2 == 0:
epd.fill_rect(130 + col * 24, 20 + ligne * 24, 24, 24, 1)
# --- Deux diagonales ---
epd.line(130, 20, 226, 116, 1)
epd.line(130, 116, 226, 20, 1)
epd.update()
epd.deep_sleep()
🤔 Avant de lancer Dans la boucle du damier, la condition est `(ligne + col) % 2 == 0`. Que se passe-t-il si tu la remplaces par `== 1` ? voir la réponse
Le damier s’inverse : les cases noires deviennent blanches et inversement. Une case est noire quand la somme ligne + colonne est paire, c’est-à-dire une case sur deux, en alternance.
- Dessine un damier de 8 × 8 cases plus petites (change 4 et 24)
- Ajoute un rectangle plein avec
fill_rect - Change le pas de la cible :
range(0, 40, 8)devientrange(0, 40, 4)
Manip 05 — Ton badge nominatif
C’est le moment de l’atelier : chacun fabrique son badge. Le programme agrandit ton prénom au maximum, centré, avec un cadre et une deuxième ligne.
manip05_badge.py .python atelier-epaper/manip05_badge.py 61 lignes
GitHub
# Atelier 02 — E-paper — Manip 05 : ton badge nominatif
# Fablab Ardèche — MicroPython
#
# Ton prénom s'affiche en aussi gros que possible, centré. Comme l'écran
# e-paper garde l'image sans courant, tu peux débrancher la carte et
# la garder comme badge, magnet ou porte-clés.
#
# Change PRENOM, puis lance le programme (F5).
#
# Matériel : carte LilyGo T5 V2.3 (écran e-paper déjà câblé)
from gdeh0213b73 import init_epd, ROTATION_90
import framebuf
PRENOM = "Camille" # ASCII seulement : pas d'accents !
LIGNE2 = "Fablab Polinno"
def big_text(epd, s, x, y, scale=2, c=1):
w = len(s) * 8
buf = bytearray(((w + 7) // 8) * 8)
fb = framebuf.FrameBuffer(buf, w, 8, framebuf.MONO_HLSB)
fb.fill(0)
fb.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if fb.pixel(tx, ty):
epd.fill_rect(x + tx * scale, y + ty * scale, scale, scale, c)
def texte_centre(epd, s, y, scale=2, c=1):
largeur = len(s) * 8 * scale
big_text(epd, s, (epd.width - largeur) // 2, y, scale, c)
def echelle_max(epd, s, maxi=6):
"""Plus grand agrandissement qui tient dans la largeur de l'écran."""
scale = maxi
while scale > 1 and len(s) * 8 * scale > epd.width - 16:
scale -= 1
return scale
epd = init_epd(rotation=ROTATION_90)
epd.fill(0)
epd.rect(0, 0, epd.width, epd.height, 1)
epd.rect(3, 3, epd.width - 6, epd.height - 6, 1)
echelle = echelle_max(epd, PRENOM)
haut = 8 * echelle # hauteur du prénom en pixels
y_prenom = 18 + (60 - haut) // 2
texte_centre(epd, PRENOM, y_prenom, scale=echelle)
epd.hline(20, 88, epd.width - 40, 1)
texte_centre(epd, LIGNE2, 98, scale=2)
epd.update()
epd.deep_sleep()
print("Badge affiché. Tu peux débrancher la carte : l'image reste.")
- Change
PRENOMetLIGNE2 - Un prénom long (10 lettres) : que fait
echelle_max? - Ajoute une ligne sous le trait, par exemple ton pseudo ou une date
- Repars avec ta carte débranchée : le badge reste affiché, sans pile
Manip 06 — Un compteur avec le bouton
Chaque appui sur le bouton de la carte (GPIO39) ajoute 1, et le nombre est redessiné en très gros.
manip06_compteur_bouton.py .python atelier-epaper/manip06_compteur_bouton.py 66 lignes
GitHub
# Atelier 02 — E-paper — Manip 06 : un compteur avec le bouton
# Fablab Ardèche — MicroPython
#
# Chaque appui sur le bouton ajoute 1 et redessine le nombre en très gros.
# Observe : l'écran met environ 2 secondes à se rafraîchir. Pendant ce temps,
# les appuis sont ignorés. C'est le prix de l'e-paper : zéro consommation
# pour garder l'image, mais un affichage lent.
#
# Matériel : carte LilyGo T5 V2.3 (écran e-paper déjà câblé)
# Bouton : celui de la carte, sur GPIO39.
from machine import Pin
from gdeh0213b73 import init_epd, ROTATION_90
import framebuf
import time
BOUTON = 39 # GPIO39 : bouton de la carte T5
bp = Pin(BOUTON, Pin.IN) # GPIO39 n'a pas de résistance de rappel interne
REPOS = bp.value() # état du bouton au repos, mesuré au démarrage
def appuye():
return bp.value() != REPOS
def big_text(epd, s, x, y, scale=2, c=1):
w = len(s) * 8
buf = bytearray(((w + 7) // 8) * 8)
fb = framebuf.FrameBuffer(buf, w, 8, framebuf.MONO_HLSB)
fb.fill(0)
fb.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if fb.pixel(tx, ty):
epd.fill_rect(x + tx * scale, y + ty * scale, scale, scale, c)
def texte_centre(epd, s, y, scale=2, c=1):
largeur = len(s) * 8 * scale
big_text(epd, s, (epd.width - largeur) // 2, y, scale, c)
def afficher(epd, valeur):
epd.fill(0)
epd.rect(0, 0, epd.width, epd.height, 1)
epd.text("Appuis sur le bouton :", 10, 10, 1)
texte_centre(epd, str(valeur), 40, scale=8) # chiffres de 64 pixels
epd.update()
# On ne met PAS l'écran en deep_sleep entre deux affichages : il faudrait le
# réveiller avec un reset matériel.
epd = init_epd(rotation=ROTATION_90)
compteur = 0
afficher(epd, compteur)
print("Appuie sur le bouton (l'écran met ~2 s à se mettre à jour)")
while True:
if appuye():
compteur += 1
print("Compteur :", compteur)
afficher(epd, compteur)
while appuye(): # attendre le relâchement
time.sleep_ms(10)
time.sleep_ms(20)
🤔 Avant de lancer Tu appuies très vite sur le bouton, 5 fois en 2 secondes. Le compteur arrive-t-il à 5 ? voir la réponse
Non, il reste plus bas. Pendant les ~2 secondes du rafraîchissement, le programme est occupé dans epd.update() et ne regarde pas le bouton : les appuis pendant ce temps sont perdus.
C’est la contrepartie de l’e-paper : zéro consommation pour garder l’image, mais un affichage lent. On l’utilise pour de l’information qui change rarement (étiquette, météo, badge), pas pour un jeu en temps réel.
- Compte à rebours : pars de 10 et retire 1 à chaque appui
- Affiche aussi le message « Bravo ! » quand le compteur atteint 10
- Change
scale=8enscale=6: combien de chiffres tiennent à l’écran ?
Avec scale=8, chaque chiffre fait 64 pixels de large : 3 chiffres (192 px) tiennent dans les 250 pixels de l’écran, mais pas 4. Au-delà de 999, l’affichage sort de l’écran.
Manip 07 — Écris sur l’écran depuis ton téléphone
C’est le grand final. La carte crée son propre réseau WiFi et un mini site web : depuis ton iPhone (ou Android), tu tapes un message, il s’affiche en gros sur l’écran, et il y reste même quand tu débranches la carte.
Avant de lancer, change NOM_RESEAU : chaque carte de la salle doit avoir un nom bien à elle, sinon on ne sait plus laquelle est la tienne.
manip07_message_iphone.py .python atelier-epaper/manip07_message_iphone.py 246 lignes
GitHub
# Atelier 02 — E-paper — Manip 07 : écris sur l'écran depuis ton iPhone
# Fablab Ardèche — MicroPython
#
# La carte crée son réseau WiFi et un mini site web. Depuis ton téléphone
# (iPhone ou Android), tu tapes un message : il s'affiche en gros sur
# l'écran e-paper, et y reste même quand tu débranches la carte.
#
# COMMENT UTILISER :
# 1. Choisis un NOM_RESEAU qui n'appartient qu'à toi (plusieurs cartes
# dans la salle : sinon on ne sait plus laquelle est laquelle)
# 2. Lance ce programme dans Thonny : l'écran affiche les instructions
# 3. Téléphone : Réglages → WiFi → ton réseau (mot de passe ci-dessous)
# 4. Ouvre Safari et tape l'adresse 192.168.4.1
# 5. Écris ton message, appuie sur « Afficher »
#
# Attention : pas d'accents sur l'écran (police ASCII). Le programme les
# remplace tout seul : é → e, ç → c, etc.
#
# Matériel : carte LilyGo T5 V2.3 (écran e-paper déjà câblé)
import network
import socket
import time
from gdeh0213b73 import init_epd, ROTATION_90
import framebuf
# --- Configuration ---
NOM_RESEAU = "Ecran-Camille" # à personnaliser
MOT_DE_PASSE = "fablab2026" # 8 caractères minimum
DELAI_MINI = 10 # secondes entre deux affichages
LONGUEUR_MAX = 60 # caractères maximum par message
ACCENTS = {
"é": "e", "è": "e", "ê": "e", "ë": "e", "à": "a", "â": "a", "ä": "a",
"î": "i", "ï": "i", "ô": "o", "ö": "o", "ù": "u", "û": "u", "ü": "u",
"ç": "c", "œ": "oe", "É": "E", "È": "E", "Ê": "E", "À": "A", "Ç": "C",
"’": "'",
}
# --- Traitement du texte reçu -------------------------------------------
def decoder(s):
"""Décode le texte d'une adresse web : « Caf%C3%A9+noir » → « Café noir »."""
s = s.replace("+", " ")
octets = bytearray()
i = 0
while i < len(s):
if s[i] == "%" and i + 2 < len(s):
try:
octets.append(int(s[i + 1:i + 3], 16))
i += 3
continue
except ValueError:
pass
octets.extend(s[i].encode())
i += 1
try:
return octets.decode("utf-8")
except UnicodeError:
return ""
def sans_accents(texte):
"""Remplace les accents, et les caractères hors ASCII par « ? »."""
resultat = ""
for c in texte:
if c in ACCENTS:
resultat += ACCENTS[c]
elif 32 <= ord(c) < 127:
resultat += c
else:
resultat += "?"
return resultat
def couper(texte, largeur):
"""Découpe un texte en lignes de 'largeur' caractères, sans couper les mots."""
lignes = []
ligne = ""
for mot in texte.split():
while len(mot) > largeur: # mot trop long : on le tronçonne
if ligne:
lignes.append(ligne)
ligne = ""
lignes.append(mot[:largeur])
mot = mot[largeur:]
if not mot:
continue
if not ligne:
ligne = mot
elif len(ligne) + 1 + len(mot) <= largeur:
ligne += " " + mot
else:
lignes.append(ligne)
ligne = mot
if ligne:
lignes.append(ligne)
return lignes
# --- Affichage sur l'écran ----------------------------------------------
def big_text(epd, s, x, y, scale=2, c=1):
w = len(s) * 8
buf = bytearray(((w + 7) // 8) * 8)
fb = framebuf.FrameBuffer(buf, w, 8, framebuf.MONO_HLSB)
fb.fill(0)
fb.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if fb.pixel(tx, ty):
epd.fill_rect(x + tx * scale, y + ty * scale, scale, scale, c)
def texte_centre(epd, s, y, scale=2, c=1):
largeur = len(s) * 8 * scale
big_text(epd, s, (epd.width - largeur) // 2, y, scale, c)
def afficher_message(epd, texte):
"""Écrit le texte le plus gros possible, sur plusieurs lignes si besoin."""
marge = 10
for scale in (6, 5, 4, 3, 2, 1):
par_ligne = (epd.width - 2 * marge) // (8 * scale)
lignes = couper(texte, par_ligne)
interligne = 2 * scale
hauteur = len(lignes) * (8 * scale + interligne) - interligne
if hauteur <= epd.height - 2 * marge:
break # cette taille tient à l'écran
epd.fill(0)
epd.rect(0, 0, epd.width, epd.height, 1)
y = (epd.height - hauteur) // 2
for ligne in lignes:
texte_centre(epd, ligne, y, scale)
y += 8 * scale + interligne
epd.update()
def afficher_instructions(epd, adresse):
epd.fill(0)
epd.rect(0, 0, epd.width, epd.height, 1)
texte_centre(epd, "Ecris-moi", 12, scale=3)
epd.text("1. WiFi : " + NOM_RESEAU, 10, 52, 1)
epd.text("2. Mot de passe : " + MOT_DE_PASSE, 10, 68, 1)
epd.text("3. Safari : " + adresse, 10, 84, 1)
epd.update()
# --- Page web ------------------------------------------------------------
def page_html(info):
return """<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>Ecran e-paper</title>
<style>
body { font-family: -apple-system, sans-serif; max-width: 420px; margin: 0 auto;
padding: 24px 16px; background: #f5f5f0; color: #222; }
h1 { font-size: 1.3rem; }
input[type=text] { width: 100%; font-size: 1.2rem; padding: 12px;
border: 2px solid #222; border-radius: 10px; box-sizing: border-box; }
button { width: 100%; margin-top: 14px; padding: 14px; font-size: 1.1rem;
font-weight: 700; background: #222; color: #fff; border: 0; border-radius: 10px; }
p.info { margin-top: 18px; padding: 10px; background: #fff; border-radius: 10px; }
</style>
</head>
<body>
<h1>Ecris sur l'ecran</h1>
<form action="/" method="get">
<input type="text" name="t" maxlength="@MAX@" placeholder="Ton message" autofocus>
<button type="submit">Afficher</button>
</form>
<p class="info">@INFO@</p>
</body>
</html>""".replace("@MAX@", str(LONGUEUR_MAX)).replace("@INFO@", info)
def repondre(conn, info):
corps = page_html(info)
conn.send("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n\r\n")
conn.send(corps)
# --- Programme principal -------------------------------------------------
ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(essid=NOM_RESEAU, password=MOT_DE_PASSE, authmode=network.AUTH_WPA_WPA2_PSK)
adresse = ap.ifconfig()[0]
print("Réseau WiFi :", NOM_RESEAU)
print("Adresse :", adresse)
# On ne met PAS l'écran en deep_sleep entre deux affichages : il faudrait le
# réveiller avec un reset matériel.
epd = init_epd(rotation=ROTATION_90)
afficher_instructions(epd, adresse)
serveur = socket.socket()
serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serveur.bind(("0.0.0.0", 80))
serveur.listen(2)
dernier = None # heure du dernier affichage (ms)
print("Serveur prêt. Ouvre http://" + adresse + " sur ton téléphone.")
while True:
conn, _ = serveur.accept()
conn.settimeout(3)
try:
requete = conn.recv(1024).decode()
chemin = requete.split(" ")[1] if " " in requete else "/"
except Exception:
conn.close()
continue
nouveau_texte = None
if chemin.startswith("/?t="):
brut = chemin[4:].split("&")[0]
texte = sans_accents(decoder(brut)).strip()[:LONGUEUR_MAX]
if not texte:
info = "Message vide ou illisible."
elif dernier is not None and time.ticks_diff(time.ticks_ms(), dernier) < DELAI_MINI * 1000:
info = "Trop vite ! L'ecran a besoin de quelques secondes de repos."
else:
nouveau_texte = texte
info = "Envoye : " + texte + ". L'ecran se met a jour (2 secondes)."
elif chemin == "/":
info = "Pret. Ecris un message ci-dessus."
else:
conn.send("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n")
conn.close()
continue
repondre(conn, info) # on répond AVANT de redessiner
conn.close()
if nouveau_texte:
print("Message :", nouveau_texte)
afficher_message(epd, nouveau_texte)
dernier = time.ticks_ms()
Une fois le programme lancé, l’écran affiche les instructions :
- Sur ton téléphone : Réglages → WiFi, choisis ton réseau et tape le mot de passe (
fablab2026). - Ouvre Safari et tape l’adresse
192.168.4.1. - Écris ton message et appuie sur Afficher. Compte environ 2 secondes.
Le réseau de la carte n’a pas d’accès à Internet, et l’iPhone le signale. C’est normal : garde-le connecté (si iOS propose « utiliser sans Internet », accepte) et ouvre l’adresse à la main dans Safari.
🤔 Avant de lancer Tu écris « Café à Payzac » avec des accents. Que voit-on sur l'écran ? voir la réponse
« Cafe a Payzac », sans accents. La police intégrée ne connaît que l’ASCII, alors le programme remplace lui-même « é » par « e », « à » par « a », « ç » par « c ». Un caractère qu’il ne sait pas remplacer (un smiley, par exemple) devient un « ? ».
- Écris un message très court (« OK »), puis un message long : comment change la taille du texte ?
- Envoie deux messages coup sur coup : que répond la page ? (indice :
DELAI_MINI) - Change
NOM_RESEAUetMOT_DE_PASSE - Fais afficher aussi l’heure d’envoi sur l’écran
- Connecte deux téléphones à la même carte : qui gagne ?
Un e-paper s’use si on le redessine sans arrêt : c’est pour ça que le programme impose un délai entre deux affichages. Pour un usage permanent, les fabricants d’écrans e-paper conseillent en général au moins 3 minutes entre deux rafraîchissements.
Bonus
« Payzac et son FabLab »
Un affichage complet, avec cadre, titre en grand et filets décoratifs.
payzac_fablab.py .python cours-exemples/epaper/payzac_fablab.py 84 lignes
GitHub
# payzac_fablab.py — LilyGo T5 V2.3 — Affiche "Payzac et son FabLab"
# -----------------------------------------------------------------------
# Bibliothèque : gdeh0213b73.py (à copier sur l'ESP32)
# fill(0) = blanc fill(1) = noir
#
# Écran 2.13" : 128 × 250 en portrait, 250 × 128 en paysage.
# Ici on travaille en PAYSAGE (ROTATION_90) : plus large, donc plus
# confortable pour du texte.
#
# ATTENTION : la police intégrée de framebuf est en ASCII pur.
# Les accents (é, è, à...) ne s'affichent pas correctement.
# D'où "Ardeche" sans accent dans la ligne du bas.
# -----------------------------------------------------------------------
from gdeh0213b73 import init_epd, ROTATION_90
import framebuf
def big_text(epd, s, x, y, scale=2, c=1):
"""Texte agrandi : scale=1 → 8px, scale=2 → 16px, scale=3 → 24px..."""
w = len(s) * 8
bpr = (w + 7) // 8
buf = bytearray(bpr * 8)
fb = framebuf.FrameBuffer(buf, w, 8, framebuf.MONO_HLSB)
fb.fill(0)
fb.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if fb.pixel(tx, ty):
epd.fill_rect(x + tx * scale, y + ty * scale, scale, scale, c)
def texte_centre(epd, s, y, scale=2, c=1):
"""Comme big_text, mais calcule x tout seul pour centrer le texte."""
largeur = len(s) * 8 * scale # largeur totale en pixels
x = (epd.width - largeur) // 2 # marge égale à gauche et à droite
big_text(epd, s, x, y, scale, c)
return x
print("Initialisation de l'ecran...")
epd = init_epd(rotation=ROTATION_90) # 250 × 128 paysage
epd.fill(0) # fond blanc
# --- Cadre extérieur (double trait) ---
epd.rect(0, 0, epd.width, epd.height, 1)
epd.rect(3, 3, epd.width - 6, epd.height - 6, 1)
# --- Le texte, centré ---
texte_centre(epd, "PAYZAC", 16, scale=4) # 32 px de haut
texte_centre(epd, "et son", 54, scale=2) # 16 px
texte_centre(epd, "FabLab", 76, scale=4) # 32 px
# --- Filets décoratifs de part et d'autre de "et son" ---
epd.hline(28, 61, 44, 1)
epd.hline(178, 61, 44, 1)
# --- Ligne du bas ---
texte_centre(epd, "Ardeche . MicroPython . ESP32", 114, scale=1)
print("Affichage en cours... (~2 s)")
epd.update() # rafraîchissement de la dalle
epd.deep_sleep() # l'image RESTE affichée, même hors tension
print("OK ! L'image reste affichee sans alimentation.")
# -----------------------------------------------------------------------
# EXPÉRIENCES À TESTER :
# - Remplace "PAYZAC" par ton prénom : texte_centre garde le centrage
# - Passe scale=4 à scale=3 → texte plus petit, plus de place
# - Enlève epd.deep_sleep() : l'image reste affichée quand même,
# mais la carte consomme beaucoup plus
# - Supprime le second epd.rect() → cadre simple au lieu de double
# - Inverse la vidéo : epd.fill(1) puis passe c=0 dans chaque appel
# → texte blanc sur fond noir
#
# À RETENIR — pourquoi un écran e-paper :
# L'image est formée par de vraies micro-billes noires et blanches
# déplacées par un champ électrique. Une fois en place, elles ne
# bougent plus : l'affichage ne consomme RIEN pour être maintenu.
# D'où les liseuses qui tiennent des semaines sur une charge.
# En contrepartie, le rafraîchissement prend environ 2 secondes.
# -----------------------------------------------------------------------
Deux cartes qui se parlent : ESP-NOW
Pas besoin de WiFi ni de box : avec ESP-NOW, deux cartes ESP32 s’envoient des messages directement. Ici, un bouton sur une carte compte les appuis, et le nombre s’affiche sur l’écran e-paper de l’autre.
Matériel en plus : une carte ESP32 simple, un bouton poussoir entre GPIO5 et GND.
1. Lire l’adresse de chaque carte. Chaque ESP32 a une adresse unique, l’adresse MAC : c’est le « numéro de téléphone » qui permet d’écrire à une carte précise. Lance ce programme sur chaque carte et note l’adresse qui s’affiche.
manip01_adresse_mac.py .python atelier-epaper/espnow/manip01_adresse_mac.py 21 lignes
GitHub
# Atelier 02 — E-paper — Bonus ESP-NOW — Manip 01 : l'adresse MAC de ta carte
# Fablab Ardèche — MicroPython
#
# Chaque carte ESP32 a une adresse unique, gravée en usine : l'adresse MAC.
# C'est elle qui permet à une carte d'en trouver une autre, comme un numéro
# de téléphone. ESP-NOW s'en sert pour envoyer un message à UNE carte précise.
#
# Lance ce programme sur chaque carte et note son adresse.
#
# Matériel : n'importe quelle carte ESP32, rien à brancher
import network
wifi = network.WLAN(network.STA_IF)
wifi.active(True) # la radio doit être allumée pour lire l'adresse
mac = wifi.config("mac") # 6 octets
texte = ":".join("{:02x}".format(o) for o in mac)
print("Adresse MAC de cette carte :", texte)
2. Lancer le récepteur sur la T5. L’écran affiche « En attente » et son adresse.
manip03_recepteur_epaper.py .python atelier-epaper/espnow/manip03_recepteur_epaper.py 106 lignes
GitHub
# Atelier 02 — E-paper — Bonus ESP-NOW — Manip 03 : le récepteur (écran e-paper)
# Fablab Ardèche — MicroPython
#
# La carte T5 reçoit le nombre envoyé par l'émetteur et l'affiche en gros
# sur l'écran e-paper, avec la force du signal.
#
# Au démarrage, l'écran affiche l'ADRESSE MAC de cette carte : c'est elle
# qu'il faut recopier dans l'émetteur (manip 02).
#
# Petit défi de conception : l'écran met ~2 s à se rafraîchir, pendant que
# le bouton peut être pressé 5 fois. Le programme ne dessine donc que le
# DERNIER nombre reçu, et jamais plus d'un affichage toutes les 3 secondes.
# Comme l'émetteur envoie le total (et pas « +1 »), un message perdu ou
# sauté ne fausse rien.
#
# Matériel : carte LilyGo T5 V2.3 (écran e-paper déjà câblé)
import network
import espnow
import time
from gdeh0213b73 import init_epd, ROTATION_90
import framebuf
DELAI_MINI = 3000 # ms minimum entre deux rafraîchissements
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
mac = ":".join("{:02x}".format(o) for o in wifi.config("mac"))
print("Adresse MAC de cette carte :", mac)
def big_text(epd, s, x, y, scale=2, c=1):
w = len(s) * 8
buf = bytearray(((w + 7) // 8) * 8)
fb = framebuf.FrameBuffer(buf, w, 8, framebuf.MONO_HLSB)
fb.fill(0)
fb.text(s, 0, 0, 1)
for ty in range(8):
for tx in range(w):
if fb.pixel(tx, ty):
epd.fill_rect(x + tx * scale, y + ty * scale, scale, scale, c)
def texte_centre(epd, s, y, scale=2, c=1):
largeur = len(s) * 8 * scale
big_text(epd, s, (epd.width - largeur) // 2, y, scale, c)
def echelle_max(epd, s, maxi=8):
"""Plus grand agrandissement qui tient dans la largeur de l'écran."""
scale = maxi
while scale > 1 and len(s) * 8 * scale > epd.width - 16:
scale -= 1
return scale
def afficher_attente(epd):
epd.fill(0)
epd.rect(0, 0, epd.width, epd.height, 1)
texte_centre(epd, "En attente", 20, scale=3)
epd.text("Adresse de cette carte :", 10, 70, 1)
texte_centre(epd, mac, 88, scale=1)
epd.update()
def afficher_nombre(epd, nombre, rssi):
epd.fill(0)
epd.rect(0, 0, epd.width, epd.height, 1)
echelle = echelle_max(epd, nombre)
texte_centre(epd, nombre, 20 + (64 - 8 * echelle) // 2, scale=echelle)
if rssi is not None:
epd.text("signal : {} dBm".format(rssi), 10, 108, 1)
epd.update()
# On ne met PAS l'écran en deep_sleep entre deux affichages : il faudrait le
# réveiller avec un reset matériel.
epd = init_epd(rotation=ROTATION_90)
afficher_attente(epd)
e = espnow.ESPNow()
e.active(True)
print("Récepteur prêt.")
affiche = None # ce qui est actuellement à l'écran
en_attente = None # dernier nombre reçu, pas encore affiché
rssi = None
dernier_rafraichissement = time.ticks_ms()
while True:
hote, msg = e.recv(200) # attend 200 ms au plus
while hote is not None: # on lit TOUT ce qui est arrivé : seul le dernier compte
en_attente = msg.decode()
try:
rssi = e.peers_table[hote][0]
except (KeyError, IndexError):
rssi = None
print("Reçu :", en_attente, "| signal :", rssi)
hote, msg = e.recv(0)
if en_attente is not None and en_attente != affiche:
if time.ticks_diff(time.ticks_ms(), dernier_rafraichissement) >= DELAI_MINI:
afficher_nombre(epd, en_attente, rssi)
affiche = en_attente
dernier_rafraichissement = time.ticks_ms()
3. Configurer l’émetteur sur l’ESP32 simple : recopie l’adresse de la T5 dans MAC_RECEPTEUR, puis lance le programme.
manip02_emetteur_bouton.py .python atelier-epaper/espnow/manip02_emetteur_bouton.py 67 lignes
GitHub
# Atelier 02 — E-paper — Bonus ESP-NOW — Manip 02 : l'émetteur (bouton qui compte)
# Fablab Ardèche — MicroPython
#
# À chaque appui sur le bouton, la carte ajoute 1 à un compteur et envoie le
# NOMBRE TOTAL à l'autre carte par ESP-NOW, sans box ni WiFi.
#
# La LED de la carte donne le résultat de l'envoi :
# - allumée 0,2 s : l'autre carte a bien accusé réception
# - clignote 3 fois : personne n'a répondu (mauvaise adresse ? trop loin ?)
#
# Matériel : carte ESP32 simple + 1 bouton poussoir
# Connexions : bouton entre GPIO5 et GND, LED intégrée sur GPIO2
#
# COMMENT UTILISER :
# 1. Lance d'abord le récepteur (manip 03) : son écran affiche son adresse
# 2. Recopie cette adresse dans MAC_RECEPTEUR ci-dessous
# 3. Lance ce programme, puis appuie sur le bouton
import network
import espnow
import time
from machine import Pin
import ubinascii
MAC_RECEPTEUR = "24:6f:28:b1:e4:2c" # à remplacer par l'adresse affichée par le récepteur
bp = Pin(5, Pin.IN, Pin.PULL_UP) # bouton : 0 = appuyé
led = Pin(2, Pin.OUT)
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
e = espnow.ESPNow()
e.active(True)
adresse = ubinascii.unhexlify(MAC_RECEPTEUR.replace(":", ""))
e.add_peer(adresse)
compteur = 0
print("Radio ESP-NOW activée, envoi vers", MAC_RECEPTEUR)
print("Prêt. Appuie sur le bouton.")
while True:
if bp.value() == 0:
compteur += 1
try:
recu = e.send(adresse, str(compteur)) # True si l'autre carte a répondu
except OSError as erreur: # l'erreur s'affiche au lieu d'arrêter le programme
print("Erreur d'envoi :", erreur)
recu = False
if recu:
print(compteur, "→ reçu")
led.value(1)
time.sleep_ms(200)
led.value(0)
else:
print(compteur, "→ PAS de réponse")
for _ in range(3):
led.value(1)
time.sleep_ms(100)
led.value(0)
time.sleep_ms(100)
time.sleep_ms(30) # anti-rebond
while bp.value() == 0: # attendre le relâchement
time.sleep_ms(10)
time.sleep_ms(30)
time.sleep_ms(10)
4. Appuie sur le bouton. La LED de l’émetteur s’allume brièvement si l’autre carte a répondu, et clignote 3 fois sinon. Quelques secondes plus tard, le nombre apparaît en gros sur l’écran, avec la force du signal.
Un programme lancé avec F5 depuis Thonny reste en mémoire et disparaît quand on coupe l’alimentation. Pour faire fonctionner une carte sur batterie, enregistre le programme sous le nom main.py directement sur la carte : Fichier → Enregistrer sous → « Appareil MicroPython ». Il démarrera alors tout seul à chaque mise sous tension.
Cela veut dire que la T5 n’a pas accusé réception. Vérifie que le récepteur tourne bien (l’écran affiche « En attente ») et que l’adresse recopiée est exactement celle affichée. Pour séparer les deux causes, remplace l’adresse par ff:ff:ff:ff:ff:ff (diffusion à toutes les cartes) : si le nombre apparaît sur l’écran de la T5, la radio fonctionne et l’adresse était fausse.
🤔 Avant de lancer Tu appuies 5 fois très vite sur le bouton. L'écran passe-t-il par 1, 2, 3, 4, 5 ? voir la réponse
Non, il affiche directement 5. L’écran met environ 2 secondes à se rafraîchir, et le récepteur ne dessine que le dernier nombre reçu, au plus une fois toutes les 3 secondes. Rien n’est perdu : l’émetteur envoie le total et non « +1 », donc un message manqué ne fausse pas le compte.
- Éloigne-toi avec l’émetteur (sur batterie) et regarde la valeur du signal en dBm : plus elle est négative, plus le signal est faible. Jusqu’où le message passe-t-il ?
- Change
DELAI_MINI(récepteur) : que se passe-t-il à 1 seconde ? à 10 secondes ? - Envoie un mot au lieu d’un nombre : que faut-il changer des deux côtés ?
- Ajoute un deuxième bouton sur l’émetteur qui remet le compteur à zéro
Comment ça marche
Pourquoi l’image reste sans courant
L’écran contient des millions de micro-capsules avec des billes noires et blanches chargées électriquement. Un champ électrique les déplace vers la surface, et une fois en place, elles ne bougent plus : maintenir l’image ne demande aucune énergie. C’est ce qui permet aux liseuses de tenir des semaines.
Pourquoi c’est lent
Pour effacer proprement l’image précédente, l’écran fait plusieurs cycles de noir et de blanc avant d’afficher la nouvelle : c’est le clignotement que tu vois. D’où les 2 secondes.
deep_sleep()
epd.deep_sleep() met le contrôleur de l’écran en veille profonde. L’image reste, la consommation tombe presque à zéro. Pour ré-afficher ensuite, par prudence, réinitialise l’écran en rappelant init_epd() avant de dessiner.
Pour aller plus loin
Étiquette de prix
Affiche un produit et son prix : idéal pour un petit commerce, l'écran ne consomme rien.
Mesure affichée
Branche un capteur de température (atelier Station météo) et affiche la valeur sur l'e-paper.
Sur batterie
Entre deux affichages, deep_sleep réduit la consommation au minimum : de quoi alimenter un affichage sur batterie.