ATELIER 02 ● Débutant

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

ESP32E-paperLilyGo T5SPIAffichage

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

ComposantQuantitéNotes
Carte LilyGo T5 V2.31ESP32 + écran e-paper 2,13” (250 × 128 pixels) déjà câblé
Câble USB1De données (pas un câble de charge seule)
Ordinateur avec Thonny1MicroPython 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 :

broches internes
SCK  → GPIO18      MOSI → GPIO23
CS   → GPIO5       DC   → GPIO17
RST  → GPIO16      BUSY → GPIO4

Déroulé (2 h)

DuréeÉtape
10 minPrésentation : qu’est-ce que l’e-paper, pourquoi ça consomme zéro
15 minManip 01 — installer la bibliothèque
10 minManip 02 — premier affichage
15 minManip 03 — agrandir le texte
10 minManip 04 — formes et motifs
20 minManip 05 — ton badge nominatif
15 minManip 06 — compteur avec le bouton
20 minManip 07 — écris sur l’écran depuis ton téléphone
5 minQuestions 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.

  1. Télécharge le fichier : gdeh0213b73.py sur GitHub (bouton « Download raw file »).
  2. Ouvre Thonny et affiche le panneau des fichiers : Affichage → Fichiers.
  3. Repère le fichier dans la partie haute (ton ordinateur), clic droit → Téléverser vers /.
  4. 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 /")
⛔ Le fichier doit être sur l'ESP32, pas sur ton ordinateur

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.

🔧 À toi de jouer
  • Remplace ROTATION_0 par ROTATION_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 ?
⚠️ Pas d'accents

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()
🔧 À toi de jouer
  • Change scale=4 en scale=2 : comment évolue la taille du texte ?
  • Écris « ESP32 » à la place de « 32 px »
  • texte_centre calcule x toute seule : essaie un texte plus long, puis un texte plus court
  • Inverse les couleurs : epd.fill(1) au départ, puis c=0 dans 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.

🔧 À toi de jouer
  • 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) devient range(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.")
🔧 À toi de jouer
  • Change PRENOM et LIGNE2
  • 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.

🔧 À toi de jouer
  • Compte à rebours : pars de 10 et retire 1 à chaque appui
  • Affiche aussi le message « Bravo ! » quand le compteur atteint 10
  • Change scale=8 en scale=6 : combien de chiffres tiennent à l’écran ?
⚠️ 4 chiffres ne tiennent pas

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 :

  1. Sur ton téléphone : Réglages → WiFi, choisis ton réseau et tape le mot de passe (fablab2026).
  2. Ouvre Safari et tape l’adresse 192.168.4.1.
  3. Écris ton message et appuie sur Afficher. Compte environ 2 secondes.
⚠️ iPhone : « pas de connexion Internet »

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 « ? ».

🔧 À toi de jouer
  • É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_RESEAU et MOT_DE_PASSE
  • Fais afficher aussi l’heure d’envoi sur l’écran
  • Connecte deux téléphones à la même carte : qui gagne ?
⚠️ L'écran n'aime pas le rafraîchissement continu

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.

⛔ Enregistre le programme sur la carte pour l'utiliser sans ordinateur

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.

⚠️ « PAS de réponse » sur l'émetteur

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.

🔧 À toi de jouer
  • É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.