AccueilAteliersInitiation ESP32
ATELIER DÉCOUVERTE Débutant

Initiation à l’ESP32 — Zéro câblage

9 manipulations progressives pour découvrir ce qu’est un microcontrôleur. Du premier dialogue avec la machine jusqu’au contrôle d’une LED depuis ton smartphone — sans brancher quoi que ce soit.

3h

ESP32MicroPythonWiFiLEDREPL

Matériel nécessaire

🎉

Rien à brancher sur breadboard

Toutes les manipulations utilisent uniquement la LED intégrée (GPIO 2) et le bouton BOOT (GPIO 0) déjà présents sur la carte.

MatérielQuantitéNotes
ESP321Fourni en atelier — tu repars avec
Câble USB1Micro-USB ou USB-C selon la carte
Ordinateur1Avec Thonny installé
Smartphone1Pour la manipulation finale

Les 9 manipulations

#ManipulationConcept clé
01Le REPL et la LEDConsole interactive, variables, GPIO sortie
02LED qui clignoteBoucle infinie, while True
03LED qui “respire”PWM, luminosité variable
04Lire le bouton BOOTGPIO en entrée, logique active bas
05LED commandée par le boutonCapteur → actionneur
06Temporisateur d’éclairageCompteur, minuterie d’escalier
07Température du processeurCapteur interne, f-strings
08Scanner les réseaux WiFiModule réseau
09Tableau de bord web + LED + températureWiFi + HTTP + PWM + capteur

Manip 01 — Le REPL et la LED

Tout se fait dans le Shell de Thonny (la console en bas). On tape une instruction, l’ESP32 l’exécute et affiche le résultat.

Découverte : calculs et variables

>>> 2 + 3 * 4                     # la multiplication est prioritaire
14
>>> 15 / 3 - 1                    # la division retourne un "float"
4.0
>>> 13 // 4                       # division entière
3
>>> 13 % 4                        # modulo (reste de la division)
1
>>> 2 ** 8                        # puissance : 2 exposant 8
256
>>> vitesse1 = 36
>>> vitesse2 = 42
>>> print("Moyenne :", (vitesse1 + vitesse2) / 2)
Moyenne : 39.0
>>> type(vitesse1)                 # int = nombre entier
<class 'int'>
>>> type("Bonjour")               # str = chaîne de caractères
<class 'str'>

Contrôler la LED intégrée

La LED bleue sur la carte est connectée au GPIO 2. On la commande depuis le Shell :

>>> from machine import Pin
>>> led = Pin(2, Pin.OUT)          # GPIO 2 en sortie
>>> led.value(1)                   # la LED s'allume !
>>> led.value(0)                   # la LED s'éteint
>>> led.on()                       # raccourci pour allumer
>>> led.off()                      # raccourci pour éteindre
>>> Pin(2, Pin.OUT).value(1)       # tout en une seule ligne

Ctrl+C : arrête le programme en cours · Ctrl+D : redémarre l’ESP32 · Flèche Haut : rappelle la dernière commande · Pin(n, Pin.OUT) = broche en sortie · value(1) = 3,3V → allumée · value(0) = 0V → éteinte


Manip 02 — Faire clignoter la LED

Le “Hello World” de l’électronique embarquée : une boucle infinie avec des pauses. Ctrl+C dans Thonny pour arrêter.

manip02_led_blink.py .python
atelier0-initiation/manip02_led_blink.py
43 lignes GitHub
# Manipulation 03 — Faire clignoter la LED (boucle infinie)
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# On combine la LED (manip 02) avec une boucle infinie et des pauses.
# C'est le "Hello World" de l'électronique embarquée.
#
# La fonction time.sleep() met le programme en pause un certain nombre
# de secondes. Pendant cette pause, l'ESP32 ne fait rien d'autre.
#
# Rien à brancher — LED intégrée sur GPIO 2.
# -----------------------------------------------------------------------

from machine import Pin    # pour contrôler les broches
import time                # pour les fonctions de pause (sleep)

# Initialisation de la LED sur GPIO 2 en sortie
led = Pin(2, Pin.OUT)

print("LED qui clignote — appuyer Ctrl+C pour arrêter")

# Boucle infinie : while True tourne indéfiniment jusqu'à Ctrl+C
while True:
    led.value(1)           # allumer la LED
    time.sleep(0.5)        # pause 0,5 seconde (500 ms)
    led.value(0)           # éteindre la LED
    time.sleep(0.5)        # pause 0,5 seconde

# -----------------------------------------------------------------------
# EXPÉRIENCES À TESTER :
#   - Changer 0.5 en 0.1 → clignotement rapide
#   - Changer 0.5 en 2.0 → clignotement lent
#   - Mettre des durées différentes : 0.1 allumé, 0.9 éteint
#     (comme un phare ou un signal morse)
#
# POUR ARRÊTER :
#   - Appuyer Ctrl+C dans Thonny → KeyboardInterrupt
#
# VERSION COMPACTE (même résultat, une seule ligne dans la boucle) :
#   while True:
#       led.value(not led.value())   # inverse l'état actuel
#       time.sleep(0.5)
# -----------------------------------------------------------------------

Manip 03 — La LED qui “respire” (PWM)

Le PWM allume et éteint la LED des centaines de fois par seconde. L’œil perçoit une luminosité intermédiaire. duty va de 0 (éteint) à 1023 (pleine puissance).

manip03_led_pwm.py .python
atelier0-initiation/manip03_led_pwm.py
55 lignes GitHub
# Manipulation 04 — LED qui "respire" avec le PWM
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# PWM = Pulse Width Modulation = Modulation de Largeur d'Impulsion
#
# Une broche numérique ne peut faire que 0V ou 3,3V — rien entre les deux.
# Le PWM simule un niveau intermédiaire en allumant/éteignant très vite
# la LED (des centaines de fois par seconde).
#
# Si la LED est allumée 50% du temps et éteinte 50% du temps,
# l'œil voit une luminosité à 50%. C'est le "duty cycle" (rapport cyclique).
#
#   duty = 0    → LED éteinte (0% du temps allumée)
#   duty = 512  → LED mi-luminosité (50% du temps allumée)
#   duty = 1023 → LED à pleine luminosité (100% du temps allumée)
#
# Rien à brancher — LED intégrée sur GPIO 2.
# -----------------------------------------------------------------------

from machine import Pin, PWM    # PWM est dans le même module que Pin
import time                     # pour les pauses

# Créer un objet PWM sur la broche GPIO 2
# freq=1000 signifie que la LED clignote 1000 fois par seconde
# (trop rapide pour que l'œil le voit — il perçoit juste la luminosité moyenne)
led = PWM(Pin(2), freq=1000)

print("LED qui respire — appuyer Ctrl+C pour arrêter")

# Boucle infinie : montée puis descente progressive
while True:
    # Montée : de 0 à 1023 par pas de 5
    for luminosite in range(0, 1023, 5):
        led.duty(luminosite)   # régler la luminosité
        time.sleep_ms(5)       # pause de 5 millisecondes entre chaque pas

    # Descente : de 1023 à 0 par pas de 5
    for luminosite in range(1023, 0, -5):
        led.duty(luminosite)
        time.sleep_ms(5)

# -----------------------------------------------------------------------
# EXPÉRIENCES À TESTER DANS LE SHELL (après Ctrl+C) :
#   >>> led.duty(0)      # éteinte
#   >>> led.duty(100)    # très faible
#   >>> led.duty(512)    # moitié
#   >>> led.duty(1023)   # pleine puissance
#
# À RETENIR :
#   - PWM permet de contrôler l'intensité lumineuse d'une LED
#   - PWM sert aussi à contrôler la vitesse d'un moteur ou la position
#     d'un servo-moteur (manips futures)
#   - sleep_ms(5) = pause de 5 millisecondes (plus précis que sleep(0.005))
# -----------------------------------------------------------------------

Manip 04 — Lire le bouton BOOT

Le bouton BOOT (GPIO 0) est sur toutes les cartes ESP32. Attention à la logique inversée : relâché = 1, appuyé = 0.

manip04_bouton_boot.py .python
atelier0-initiation/manip04_bouton_boot.py
48 lignes GitHub
# Manipulation 05 — Lire le bouton BOOT (entrée numérique)
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# Toutes les cartes ESP32 ont un bouton physique appelé "BOOT" (ou "IO0"),
# connecté au GPIO 0. On va lire son état.
#
# Logique inversée (pull-up) :
#   - Bouton RELÂCHÉ → la broche lit 1 (3,3V)
#   - Bouton APPUYÉ  → la broche lit 0 (0V, reliée à la masse)
#
# C'est la logique "active bas" : l'action donne 0, le repos donne 1.
# Elle est très courante en électronique.
#
# Rien à brancher — le bouton BOOT est déjà sur la carte.
# -----------------------------------------------------------------------

from machine import Pin    # pour contrôler les broches
import time                # pour les pauses

# Initialisation du bouton BOOT sur GPIO 0 en ENTRÉE avec résistance pull-up
# Pin.PULL_UP active une résistance interne qui maintient la broche à 3,3V
# quand rien n'est connecté (évite les lectures parasites)
bouton = Pin(0, Pin.IN, Pin.PULL_UP)

print("Lecture du bouton BOOT — appuyer Ctrl+C pour arrêter")
print("Appuyez sur le bouton BOOT pour voir la valeur changer...")

while True:
    etat = bouton.value()  # lire l'état : 1 = relâché, 0 = appuyé

    if etat == 0:          # bouton appuyé (logique inversée)
        print("APPUYÉ  → valeur =", etat)
    else:                  # bouton relâché
        print("relâché → valeur =", etat)

    time.sleep(0.2)        # pause 200 ms pour ne pas inonder la console

# -----------------------------------------------------------------------
# À RETENIR :
#   - Pin.IN  = broche configurée en entrée (on lit)
#   - Pin.OUT = broche configurée en sortie (on écrit)
#   - Pin.PULL_UP = résistance interne vers 3,3V (évite les flottements)
#   - Logique active bas : 0 = action, 1 = repos
#
# DANS LE SHELL :
#   >>> bouton.value()     # lire l'état instantané
# -----------------------------------------------------------------------

Manip 05 — La LED obéit au bouton

Premier automatisme : une entrée (bouton) commande une sortie (LED) en temps réel.

manip05_led_bouton.py .python
atelier0-initiation/manip05_led_bouton.py
48 lignes GitHub
# Manipulation 06 — La LED obéit au bouton (entrée → sortie)
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# On combine les deux manips précédentes :
#   - Bouton BOOT sur GPIO 0 (entrée)
#   - LED intégrée sur GPIO 2 (sortie)
#
# Le programme lit en permanence l'état du bouton et allume ou éteint
# la LED en conséquence. C'est la base de tout automatisme :
#   un CAPTEUR commande un ACTIONNEUR.
#
# Attention à la logique inversée du bouton :
#   bouton.value() == 0  →  bouton APPUYÉ   →  LED ALLUMÉE
#   bouton.value() == 1  →  bouton RELÂCHÉ  →  LED ÉTEINTE
#
# Rien à brancher — bouton et LED sont déjà sur la carte.
# -----------------------------------------------------------------------

from machine import Pin    # pour contrôler les broches
import time                # pour les pauses

# Initialisation des broches
led    = Pin(2, Pin.OUT)              # LED en sortie sur GPIO 2
bouton = Pin(0, Pin.IN, Pin.PULL_UP)  # bouton BOOT en entrée sur GPIO 0

print("Maintenez le bouton BOOT appuyé pour allumer la LED")
print("Appuyer Ctrl+C pour arrêter")

while True:
    if bouton.value() == 0:    # bouton appuyé (logique active bas)
        led.value(1)           # allumer la LED
    else:                      # bouton relâché
        led.value(0)           # éteindre la LED

    time.sleep(0.02)           # pause 20 ms — anti-rebond simple

# -----------------------------------------------------------------------
# VERSION UNE LIGNE (même résultat, plus compact) :
#   while True:
#       led.value(not bouton.value())   # not inverse : 0→1, 1→0
#       time.sleep(0.02)
#
# POUR ALLER PLUS LOIN :
#   Modifier le code pour que le bouton BASCULE la LED à chaque appui
#   (appui 1 → allume, appui 2 → éteint, appui 3 → allume...)
#   C'est ce qu'on appelle un "bascule" ou "flip-flop".
# -----------------------------------------------------------------------

Manip 06 — Temporisateur d’éclairage

On appuie sur le bouton → la LED s’allume 2 secondes puis s’éteint seule. Si on réappuie pendant que c’est allumé, le compteur repart — exactement comme une minuterie d’escalier.

Le principe : un compteur démarre à 20 à chaque appui. À chaque tour de boucle (0,1 s), le compteur descend de 1. La LED est allumée tant que le compteur est positif.

manip06_temporisateur.py .python
atelier0-initiation/manip06_temporisateur.py
64 lignes GitHub
# Manipulation 06 — Temporisateur d'éclairage (minuterie d'escalier)
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# Principe : on appuie sur le bouton → la LED s'allume pendant 2 secondes
# puis s'éteint toute seule. Si on réappuie pendant que c'est allumé,
# le compteur repart à zéro (la durée est prolongée).
#
# Méthode : un COMPTEUR qui démarre à 20 à chaque appui.
# À chaque tour de boucle (toutes les 0,1 s), le compteur descend de 1.
# La LED est allumée tant que le compteur est positif.
#   → 20 × 0,1 s = 2 secondes d'éclairage.
#
# C'est exactement le principe d'une minuterie d'escalier :
# on appuie → la lumière reste allumée un certain temps → elle s'éteint.
#
# Rien à brancher — LED GPIO 2, bouton BOOT GPIO 0.
# -----------------------------------------------------------------------

from machine import Pin
from time import sleep

led = Pin(2, Pin.OUT)
bp  = Pin(0, Pin.IN, Pin.PULL_UP)   # bouton BOOT, actif bas (0 = appuyé)

compt = 0                           # compteur de temporisation

print("Temporisateur — appuyer sur BOOT pour allumer 2 secondes")
print("Ctrl+C pour arrêter")

while True:
    # Si le bouton est appuyé (actif bas → value() == 0), relancer le compteur
    if bp.value() == 0:
        compt = 20                   # 20 itérations × 0,1 s = 2 secondes

    # La LED est allumée tant que le compteur est positif
    led.value(compt > 0)             # compt > 0 → True (1) → LED ON

    sleep(0.1)                       # chaque tour dure 0,1 seconde

    # Décrémenter le compteur (sans passer en négatif)
    compt -= (compt > 0)             # astuce : (compt > 0) vaut 1 ou 0

# -----------------------------------------------------------------------
# COMMENT ÇA MARCHE :
#
#   compt = 20  →  led ON  →  sleep 0.1  →  compt = 19
#   compt = 19  →  led ON  →  sleep 0.1  →  compt = 18
#   ...
#   compt =  1  →  led ON  →  sleep 0.1  →  compt = 0
#   compt =  0  →  led OFF →  sleep 0.1  →  compt reste 0
#
# Si on appuie à compt = 5, le compteur remonte à 20 → relance le cycle.
#
# L'astuce "compt -= (compt > 0)" :
#   Si compt > 0, l'expression vaut True (= 1), on soustrait 1.
#   Si compt == 0, l'expression vaut False (= 0), on soustrait 0.
#   → Le compteur ne descend jamais en dessous de 0.
#
# VARIANTES À ESSAYER :
#   compt = 50   →  5 secondes d'éclairage
#   compt = 100  →  10 secondes
#   sleep(0.05)  →  plus de précision (doubler compt en conséquence)
# -----------------------------------------------------------------------

Manip 07 — Température interne du processeur

L’ESP32 a un capteur de température intégré dans le chip. Il mesure la chaleur du processeur (pas la température ambiante). Approcher le doigt près de la puce fait monter la valeur.

manip07_temperature_interne.py .python
atelier0-initiation/manip07_temperature_interne.py
53 lignes GitHub
# Manipulation 07 — Lire la température interne du processeur
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# L'ESP32 intègre un capteur de température à l'intérieur même du chip.
# Il mesure la température du processeur (pas la température ambiante).
#
# À quoi ça sert ?
#   - Surveiller que le processeur ne surchauffe pas
#   - Démontrer qu'un microcontrôleur peut avoir des capteurs intégrés
#
# ATTENTION : la valeur est en degrés Fahrenheit dans MicroPython.
#             On la convertit en Celsius avec la formule : (F - 32) / 1,8
#
# Astuce de démonstration : approcher le doigt (ou un objet chaud)
# près de la puce ESP32 pendant quelques secondes → la valeur monte.
#
# Rien à brancher — capteur interne au chip.
# -----------------------------------------------------------------------

import esp32    # module spécifique à l'ESP32, contient les fonctions internes
import time     # pour les pauses

print("Température interne du processeur ESP32")
print("Approchez le doigt près de la puce pour voir la valeur monter...")
print("Appuyer Ctrl+C pour arrêter")
print()

while True:
    # Lecture de la température brute (en Fahrenheit)
    temp_fahrenheit = esp32.raw_temperature()

    # Conversion en Celsius
    temp_celsius = (temp_fahrenheit - 32) / 1.8

    # Affichage avec une décimale
    print(f"Température : {temp_celsius:.1f} °C  ({temp_fahrenheit} °F)")

    time.sleep(1)    # une mesure par seconde

# -----------------------------------------------------------------------
# À RETENIR :
#   - Le module "esp32" donne accès aux fonctions spéciales de la puce
#   - raw_temperature() retourne des degrés Fahrenheit (héritage américain)
#   - Formule de conversion : °C = (°F - 32) / 1.8
#   - f"..." = f-string : façon moderne d'insérer des variables dans du texte
#   - :.1f = formater un nombre avec 1 décimale
#
# DANS LE SHELL :
#   >>> import esp32
#   >>> esp32.raw_temperature()
#   >>> (esp32.raw_temperature() - 32) / 1.8
# -----------------------------------------------------------------------

Manip 08 — Scanner les réseaux WiFi

L’ESP32 détecte tous les réseaux WiFi environnants et affiche leur nom et niveau de signal. Dans une salle remplie d’ESP32, l’effet est garanti.

manip08_scan_wifi.py .python
atelier0-initiation/manip08_scan_wifi.py
59 lignes GitHub
# Manipulation 08 — Scanner les réseaux WiFi environnants
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# L'ESP32 est équipé d'une antenne WiFi intégrée.
# Il peut se connecter à un réseau existant (mode Station / STA)
# ou créer son propre réseau (mode Access Point / AP).
#
# Dans cette manip, on utilise le mode Station juste pour écouter :
# on cherche les réseaux WiFi disponibles dans la salle.
# Chaque réseau trouvé affiche son nom (SSID) et son niveau de signal.
#
# Le signal WiFi se mesure en dBm (décibels milliwatts) :
#   -30 dBm → signal excellent (très près du routeur)
#   -70 dBm → signal faible
#   -90 dBm → signal limite
#
# Rien à brancher.
# -----------------------------------------------------------------------

import network    # module pour tout ce qui concerne le WiFi et les réseaux
import time       # pour les pauses

# Créer une interface WiFi en mode "Station" (client, pas serveur)
wifi = network.WLAN(network.STA_IF)
wifi.active(True)    # activer le module WiFi

print("Scan des réseaux WiFi en cours...")
print()

# scan() retourne une liste de tuples :
# (ssid, bssid, canal, signal, securite, cache)
reseaux = wifi.scan()

print(f"{len(reseaux)} réseaux trouvés :")
print("-" * 40)

for reseau in reseaux:
    nom    = reseau[0].decode("utf-8")  # nom du réseau (bytes → texte)
    signal = reseau[3]                  # niveau de signal en dBm
    canal  = reseau[2]                  # numéro de canal WiFi (1 à 13)

    # Barre de signal visuelle (de 0 à 5 barres)
    force = max(0, min(5, (signal + 100) // 10))
    barres = "" * force + "" * (5 - force)

    print(f"  {barres}  {signal:4d} dBm  canal {canal:2d}  {nom}")

print()
print("Scan terminé.")

# -----------------------------------------------------------------------
# À RETENIR :
#   - network.WLAN() crée une interface WiFi
#   - network.STA_IF = mode Station (se connecte à un réseau existant)
#   - network.AP_IF  = mode Access Point (crée son propre réseau)
#   - scan() retourne une liste de tuples avec les infos de chaque réseau
#   - .decode("utf-8") convertit des bytes en texte lisible
# -----------------------------------------------------------------------

Manip 09 — Tableau de bord web ✨

Le grand final. L’ESP32 crée son réseau WiFi et sert un vrai tableau de bord avec :

Comment faire :

  1. Lancer ce programme dans Thonny
  2. Smartphone : réglages WiFi → se connecter à ESP32-Polinno (mot de passe : micropython)
  3. Navigateur → taper 192.168.4.1
manip09_serveur_web.py .python
atelier0-initiation/manip09_serveur_web.py
361 lignes GitHub
# Manipulation 09 — Tableau de bord ESP32 (serveur web)
# Fablab Ardèche — Atelier d'initiation
# -----------------------------------------------------------------------
# Le grand final : l'ESP32 crée son réseau WiFi et sert un tableau de bord :
#   - Contrôle ON/OFF de la LED intégrée
#   - Slider de luminosité (PWM) — glisser le doigt = LED qui varie
#   - Température du processeur en temps réel (toutes les 3s)
#
# COMMENT UTILISER :
#   1. Lancer ce programme dans Thonny
#   2. Smartphone : WiFi → "ESP32-Polinno" (mot de passe : micropython)
#   3. Navigateur → 192.168.4.1
#
# Rien à brancher — LED intégrée sur GPIO 2.
#
# Routes :
#   GET /         → page tableau de bord
#   GET /on       → LED pleine puissance, redirect /
#   GET /off      → LED éteinte, redirect /
#   GET /dim?v=N  → LED à N% de puissance (0-100), pas de redirect
#   GET /temp     → température en texte brut (pour le JavaScript)

import network
import socket
import esp32
from machine import Pin, PWM

# --- Configuration ---
SSID    = "ESP32-Polinno"
PASSWORD = "micropython"

# --- LED en mode PWM (permet de varier la luminosité) ---
# PWM sur GPIO2, fréquence 1000 Hz
# duty : 0 = éteinte, 1023 = pleine puissance
led = PWM(Pin(2), freq=1000)
led.duty(0)
luminosite  = 100   # luminosité mémorisée en % (pour le bouton "Allumer")
led_allumee = False

# --- Point d'accès WiFi ---
ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(essid=SSID, password=PASSWORD)
print("Réseau WiFi :", SSID)
print("Adresse IP  :", ap.ifconfig()[0])


def pct_to_duty(pct):
    """Convertit un pourcentage (0-100) en valeur PWM (0-1023) avec correction gamma.

    Sans correction : à 50% le slider la LED paraît déjà très lumineuse
    car l'œil humain perçoit la luminosité de façon logarithmique.
    La correction gamma 2.2 rend la progression du slider linéaire
    pour la perception visuelle.

    Formule : duty = (pct / 100) ^ 2.2 × 1023
    """
    if pct <= 0:
        return 0
    if pct >= 100:
        return 1023
    return int((pct / 100) ** 2 * 1023)


def get_temp():
    """Retourne la température du processeur en °C."""
    return f"{(esp32.raw_temperature() - 32) / 1.8:.1f}"


def page_html(led_allumee, luminosite):
    """Génère la page tableau de bord."""
    etat   = "ALLUMÉE"  if led_allumee else "ÉTEINTE"
    couleur = "#22c55e" if led_allumee else "#374151"
    glow    = "0 0 20px #22c55e88, 0 0 40px #22c55e33" if led_allumee else "none"
    temp   = get_temp()

    return f"""<!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>ESP32 Fablab</title>
  <style>
    * {{ box-sizing: border-box; margin: 0; padding: 0; }}
    body {{
      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
      background: #0f0f1a;
      color: #e2e8f0;
      padding: 20px 16px 40px;
      max-width: 400px;
      margin: 0 auto;
    }}
    header {{
      text-align: center;
      padding: 20px 0 24px;
      border-bottom: 1px solid #1e293b;
      margin-bottom: 20px;
    }}
    header h1 {{
      font-size: 0.9rem;
      color: #38bdf8;
      letter-spacing: 3px;
      font-weight: 700;
      text-transform: uppercase;
    }}
    header p {{ color: #475569; font-size: 0.75rem; margin-top: 4px; }}

    .card {{
      background: #1e293b;
      border: 1px solid #334155;
      border-radius: 18px;
      padding: 22px 20px;
      margin-bottom: 14px;
    }}
    .card-label {{
      font-size: 0.65rem;
      color: #64748b;
      text-transform: uppercase;
      letter-spacing: 1.5px;
      margin-bottom: 16px;
    }}

    /* --- Carte LED --- */
    .led-row {{
      display: flex;
      align-items: center;
      gap: 16px;
      margin-bottom: 18px;
    }}
    .led-orb {{
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background: {couleur};
      box-shadow: {glow};
      flex-shrink: 0;
      transition: background 0.3s, box-shadow 0.3s;
    }}
    .led-info {{ flex: 1; }}
    .led-etat {{
      font-size: 1.1rem;
      font-weight: 700;
      color: {'#22c55e' if led_allumee else '#64748b'};
    }}
    .led-sub {{ font-size: 0.72rem; color: #475569; margin-top: 2px; }}

    .btns {{ display: flex; gap: 10px; }}
    .btn {{
      flex: 1;
      display: block;
      padding: 12px;
      border-radius: 12px;
      font-size: 0.95rem;
      font-weight: 700;
      text-decoration: none;
      text-align: center;
    }}
    .btn:active {{ opacity: 0.75; }}
    .btn-on  {{ background: #22c55e; color: #fff; }}
    .btn-off {{ background: #ef4444; color: #fff; }}

    /* --- Slider luminosité --- */
    .slider-row {{
      display: flex;
      align-items: center;
      gap: 14px;
      margin-bottom: 6px;
    }}
    .slider-row input {{
      flex: 1;
      accent-color: #f59e0b;
      height: 36px;
      cursor: pointer;
      -webkit-appearance: none;
      appearance: none;
      background: transparent;
    }}
    .slider-row input::-webkit-slider-runnable-track {{
      height: 12px;
      background: #334155;
      border-radius: 6px;
    }}
    .slider-row input::-webkit-slider-thumb {{
      -webkit-appearance: none;
      width: 44px;
      height: 44px;
      border-radius: 50%;
      background: #f59e0b;
      margin-top: -16px;
      box-shadow: 0 2px 8px #0008;
    }}
    .slider-val {{
      font-size: 1.3rem;
      font-weight: 700;
      color: #f59e0b;
      width: 52px;
      text-align: right;
    }}
    .slider-labels {{
      display: flex;
      justify-content: space-between;
      font-size: 0.65rem;
      color: #475569;
      margin-top: 4px;
    }}

    /* --- Carte température --- */
    .temp-row {{
      display: flex;
      align-items: baseline;
      gap: 6px;
    }}
    .temp-val {{
      font-size: 2.4rem;
      font-weight: 800;
      color: #38bdf8;
      line-height: 1;
    }}
    .temp-unit {{ font-size: 1rem; color: #64748b; }}
    .temp-sub  {{ font-size: 0.72rem; color: #475569; margin-top: 6px; }}

    footer {{
      text-align: center;
      color: #1e293b;
      font-size: 0.7rem;
      margin-top: 24px;
      font-family: monospace;
    }}
  </style>
</head>
<body>

  <header>
    <h1>ESP32 · Fablab Polinno</h1>
    <p>Serveur web embarqué · 192.168.4.1</p>
  </header>

  <!-- Contrôle LED -->
  <div class="card">
    <div class="card-label">Contrôle LED · GPIO 2</div>
    <div class="led-row">
      <div class="led-orb" id="orb"></div>
      <div class="led-info">
        <div class="led-etat" id="etat">LED {etat}</div>
        <div class="led-sub">LED intégrée</div>
      </div>
    </div>
    <div class="btns">
      <a href="/on"  class="btn btn-on" >Allumer</a>
      <a href="/off" class="btn btn-off">Éteindre</a>
    </div>
  </div>

  <!-- Luminosité (PWM) -->
  <div class="card">
    <div class="card-label">Luminosité · PWM</div>
    <div class="slider-row">
      <input type="range" id="slider" min="0" max="100" value="{luminosite}">
      <div class="slider-val"><span id="pct">{luminosite}</span>%</div>
    </div>
    <div class="slider-labels">
      <span>Éteint</span>
      <span>Pleine puissance</span>
    </div>
  </div>

  <!-- Température -->
  <div class="card">
    <div class="card-label">Température · Processeur interne</div>
    <div class="temp-row">
      <div class="temp-val" id="temp">{temp}</div>
      <div class="temp-unit">°C</div>
    </div>
    <div class="temp-sub">Mise à jour toutes les 3 secondes</div>
  </div>

  <footer>MicroPython · ESP32 · Fablab Polinno</footer>

  <script>
    var slider = document.getElementById('slider');
    var pct    = document.getElementById('pct');

    // Mise à jour de l'affichage en temps réel pendant le glissement
    slider.oninput = function() {{
      pct.textContent = this.value;
    }};

    // Envoi de la valeur à l'ESP32 quand le doigt se lève
    slider.onchange = function() {{
      var v = this.value;
      pct.textContent = v;
      fetch('/dim?v=' + v).catch(function() {{}});
    }};

    // Température live toutes les 3 secondes
    setInterval(function() {{
      fetch('/temp')
        .then(function(r) {{ return r.text(); }})
        .then(function(v) {{ document.getElementById('temp').textContent = v; }})
        .catch(function() {{}});
    }}, 3000);
  </script>

</body>
</html>"""


# --- Serveur web ---
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # libère le port si déjà utilisé
s.bind(("", 80))
s.listen(5)
print("Serveur démarré — http://192.168.4.1")

while True:
    conn, addr = s.accept()
    request = conn.recv(1024).decode()
    ligne   = request.split('\r\n')[0]   # première ligne uniquement

    if "GET /on" in ligne:
        led.duty(pct_to_duty(luminosite))
        led_allumee = True
        print("LED allumée")
        conn.send("HTTP/1.1 303 See Other\r\nLocation: /\r\nConnection: close\r\n\r\n")

    elif "GET /off" in ligne:
        led.duty(0)
        led_allumee = False
        print("LED éteinte")
        conn.send("HTTP/1.1 303 See Other\r\nLocation: /\r\nConnection: close\r\n\r\n")

    elif "GET /dim" in ligne:
        # Extraire la valeur : "GET /dim?v=75 HTTP/1.1" → 75
        try:
            v = int(ligne.split("v=")[1].split(" ")[0])
            v = max(0, min(100, v))
            luminosite  = v
            led_allumee = (v > 0)
            led.duty(pct_to_duty(v))
            print(f"Luminosité : {v}%")
        except Exception:
            pass
        conn.send("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n")

    elif "GET /temp" in ligne:
        t = get_temp()
        conn.send(f"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n{t}")

    elif "GET / " in ligne:
        html = page_html(led_allumee, luminosite)
        conn.send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n")
        # Envoi par morceaux de 512 octets (la page est trop grande pour un seul send)
        for i in range(0, len(html), 512):
            conn.send(html[i:i + 512])

    else:
        conn.send("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n")

    conn.close()

Pour aller plus loin

LED externe

Brancher une LED sur breadboard avec une résistance 220Ω pour une lumière plus visible.

Ajouter un capteur

Affiche la température d'un DHT22 directement sur le tableau de bord.

Plusieurs LEDs

Contrôle 3 LEDs de couleurs différentes depuis la même interface web.