Illustre les modules 7 à 9 (6 figures)

M7 : modes Wi-Fi, requête HTTP. M8 : échanges navigateur/serveur, HTTP vs WebSocket. M9 : modèle GATT, session BLE. Diagrammes de séquence plus lisibles (étiquettes sur fond blanc, boîtes à la largeur du nom). Gabarit : figures pleine largeur par défaut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Artemia
2026-10-18 15:20:12 +02:00
co-authored by Claude Opus 5.5
parent 372c5a3e1e
commit f4a80f4d5b
20 changed files with 13937 additions and 12 deletions
+3
View File
@@ -44,3 +44,6 @@
// Texte des tableaux aligné à gauche (la justification crée des blancs dans les colonnes étroites)
#show table: set par(justify: false)
// Figures : pleine largeur par défaut (une largeur explicite dans le .qmd reste prioritaire)
#set image(width: 100%)
+4
View File
@@ -17,6 +17,8 @@ title: "Module 7 — Wi-Fi : station, point d'accès et client HTTP"
| `WIFI_AP` (Point d'accès) | La carte crée son propre réseau Wi-Fi | Configuration sans routeur, projet autonome |
| `WIFI_AP_STA` | Les deux simultanément | Portail de configuration puis bascule en client |
![Les trois modes Wi-Fi de l'ESP32-S3](../schemas/svg/m07-modes-wifi.svg){#fig-m07-modes-wifi}
## 2. Mode station : se connecter à un réseau existant
```cpp
@@ -74,6 +76,8 @@ void loop() {
Une fois connectée en mode station, la carte peut effectuer des requêtes HTTP vers des services web, par exemple pour récupérer la météo actuelle.
![Requête HTTP vers une API météo : connexion au réseau, GET, réponse JSON](../schemas/svg/m07-requete-http.svg){#fig-m07-requete-http}
### 4.1 Bibliothèque nécessaire
HTTPClient est incluse nativement dans le core Arduino-ESP32. Pour analyser une réponse JSON, installez la bibliothèque « ArduinoJson » (Benoit Blanchon) depuis le gestionnaire de bibliothèques.
+4
View File
@@ -55,6 +55,8 @@ void loop() {
Construisons une page avec des boutons permettant de changer la couleur de la LED NeoPixel intégrée, directement depuis un navigateur.
![Échanges entre le navigateur et le serveur embarqué lors d'un clic sur « Rouge »](../schemas/svg/m08-echanges-serveur.svg){#fig-m08-echanges-serveur}
```cpp
#include <WiFi.h>
#include <WebServer.h>
@@ -107,6 +109,8 @@ Le mot-clé PROGMEM place la chaîne de caractères HTML directement en mémoire
Le modèle utilisé ci-dessus (rechargement de page à chaque clic) fonctionne mais reste rudimentaire. Le WebSocket établit une connexion persistante entre le navigateur et la carte, permettant un échange de données en temps réel sans recharger la page — idéal pour afficher en continu une mesure de capteur ou réagir instantanément à une action utilisateur.
![HTTP classique (le navigateur redemande) et WebSocket (connexion permanente, envoi dans les deux sens)](../schemas/svg/m08-http-vs-websocket.svg){#fig-m08-http-vs-websocket}
Cette technique nécessite la bibliothèque tierce « ESPAsyncWebServer » couplée à « AsyncTCP », plus avancée que WebServer.h. Elle sera abordée en détail dans le cadre du projet final (Module 12) si le projet choisi le nécessite (ex : dashboard de supervision en direct).
## 5. Exercice : dashboard de supervision
+4
View File
@@ -25,6 +25,8 @@ Contrairement à l'ESP32 « original », l'ESP32-S3 ne supporte pas le Bluetooth
| Caractéristique | Une donnée précise à l'intérieur d'un service (ex : une couleur, une température), également identifiée par un UUID |
| Notification | Mécanisme permettant au serveur d'envoyer une mise à jour au client sans que celui-ci ait à demander (poll) |
![Modèle GATT : un serveur expose des services, qui contiennent des caractéristiques](../schemas/svg/m09-hierarchie-gatt.svg){#fig-m09-hierarchie-gatt}
::: {.callout-note title="Qu'est-ce qu'un UUID ?"}
Un UUID (Universally Unique Identifier) est un identifiant sur 128 bits garantissant qu'aucun autre service ou caractéristique dans le monde n'utilisera accidentellement le même identifiant. Pour vos projets personnels, générez vos propres UUID aléatoires via un générateur en ligne plutôt que de réutiliser des exemples trouvés sur Internet.
:::
@@ -137,6 +139,8 @@ Sans développer d'application dédiée, on peut tester un serveur BLE avec une
À l'inverse de l'écriture (client → serveur), les notifications permettent au serveur d'envoyer spontanément une donnée au client sans attendre une requête (par exemple, la valeur d'un capteur mise à jour toutes les secondes).
![Déroulé d'une session BLE avec nRF Connect : annonce, connexion, écriture, notifications](../schemas/svg/m09-sequence-ble.svg){#fig-m09-sequence-ble width=85%}
```cpp
// A ajouter a la caracteristique lors de sa creation :
// BLECharacteristic::PROPERTY_NOTIFY en plus de READ/WRITE
+54
View File
@@ -0,0 +1,54 @@
"""Module 7 — Wi-Fi : station, point d'accès et client HTTP."""
from outils import boite, fleche, sequence, toile
from style import BLEU, ENCRE, GRIS, ORANGE, VERT, Figure
def modes_wifi():
fig, ax = toile(16, 6.4)
panneaux = [(2.7, "WIFI_STA\nstation"), (8.0, "WIFI_AP\npoint d'accès"), (13.3, "WIFI_AP_STA\nles deux")]
for x, titre in panneaux:
boite(ax, x, 3.1, 5.0, 5.6, couleur=GRIS, fond="#fafafa")
ax.text(x, 5.4, titre, ha="center", va="center", fontsize=8.5, fontweight="bold", color=ENCRE)
# STA
box = boite(ax, 2.7, 3.9, 2.2, 0.8, "Box / routeur", couleur=GRIS, taille=8)
esp = boite(ax, 1.5, 2.0, 2.0, 0.8, "ESP32-S3", couleur=BLEU, taille=8, gras=True)
tel = boite(ax, 3.9, 2.0, 1.8, 0.8, "PC / mobile", couleur=GRIS, taille=8)
fleche(ax, esp["h"], (2.3, box["b"][1]), double=True, couleur=BLEU)
fleche(ax, tel["h"], (3.1, box["b"][1]), double=True, couleur=GRIS)
ax.text(2.7, 4.5, "↑ Internet", ha="center", fontsize=7.5, color=GRIS)
ax.text(2.7, 0.7, "IP donnée par la box\nWiFi.localIP()", ha="center", fontsize=7.2, color=BLEU)
# AP
esp2 = boite(ax, 8.0, 3.9, 2.2, 0.9, "ESP32-S3\n192.168.4.1", couleur=BLEU, taille=8, gras=True)
for dx in (-1.3, 1.3):
t = boite(ax, 8.0 + dx, 2.0, 1.8, 0.8, "mobile", couleur=GRIS, taille=8)
fleche(ax, t["h"], (8.0 + dx * 0.4, esp2["b"][1]), double=True, couleur=ORANGE)
ax.text(8.0, 0.7, "réseau « ESP32-Formation »\nsans box ni Internet", ha="center", fontsize=7.2, color=ORANGE)
# AP_STA
box3 = boite(ax, 13.3, 4.3, 2.2, 0.7, "Box / routeur", couleur=GRIS, taille=8)
esp3 = boite(ax, 13.3, 2.95, 2.2, 0.8, "ESP32-S3", couleur=BLEU, taille=8, gras=True)
t3 = boite(ax, 13.3, 1.5, 1.8, 0.7, "mobile", couleur=GRIS, taille=8)
fleche(ax, esp3["h"], box3["b"], "client", double=True, couleur=BLEU, taille=7)
fleche(ax, t3["h"], esp3["b"], "point d'accès", double=True, couleur=ORANGE, taille=7)
ax.text(13.3, 0.7, "ex. portail de configuration", ha="center", fontsize=7.2, color=GRIS)
return fig
def requete_http():
acteurs = ["ESP32-S3", "Box Wi-Fi", "api.open-meteo.com"]
messages = [
("ESP32-S3", "Box Wi-Fi", "WiFi.begin(ssid, motDePasse)"),
("Box Wi-Fi", "ESP32-S3", "connecté : adresse IP attribuée", True),
("ESP32-S3", "api.open-meteo.com", "http.GET() → GET /v1/forecast?latitude=…"),
("api.open-meteo.com", "ESP32-S3", "200 OK + {\"current_weather\": {\"temperature\": 14.2, …}}", True),
("ESP32-S3", "ESP32-S3", "deserializeJson() puis affichage OLED"),
("ESP32-S3", "ESP32-S3", "http.end()"),
]
fig, ax = sequence(acteurs, messages, largeur_cm=16, pas_cm=0.95,
couleurs={"Box Wi-Fi": GRIS, "api.open-meteo.com": VERT})
return fig
FIGURES = [
Figure("m07-modes-wifi", modes_wifi),
Figure("m07-requete-http", requete_http),
]
+58
View File
@@ -0,0 +1,58 @@
"""Module 8 — Serveur web embarqué."""
from matplotlib.patches import Rectangle
from outils import boite, fleche, sequence, toile
from style import BLEU, GRIS, ORANGE, ROUGE, VERT, Figure
def echanges_serveur():
acteurs = ["Navigateur (mobile)", "ESP32-S3 — WebServer", "LED NeoPixel"]
messages = [
("Navigateur (mobile)", "ESP32-S3 — WebServer", "GET / (saisie de http://192.168.1.42)"),
("ESP32-S3 — WebServer", "Navigateur (mobile)", "200 OK + page HTML (4 boutons)", True),
("Navigateur (mobile)", "ESP32-S3 — WebServer", "clic « Rouge » → GET /rouge"),
("ESP32-S3 — WebServer", "LED NeoPixel", "changerCouleur(255, 0, 0)"),
("ESP32-S3 — WebServer", "Navigateur (mobile)", "200 OK + page HTML (rechargée)", True),
]
fig, ax = sequence(acteurs, messages, largeur_cm=16, pas_cm=0.95,
couleurs={"Navigateur (mobile)": GRIS, "LED NeoPixel": ROUGE})
ax.text(8.0, 0.15, "serveur.on(\"/rouge\", …) associe chaque URL (route) à une fonction ; "
"handleClient() traite les requêtes dans loop()", ha="center", fontsize=7.2, color=GRIS, style="italic")
return fig
def http_vs_websocket():
fig, ax = toile(16, 7.2)
for x0, titre, c in ((0.3, "HTTP classique", ORANGE), (8.3, "WebSocket", VERT)):
ax.text(x0 + 3.7, 6.85, titre, ha="center", fontsize=10, fontweight="bold", color=c)
for dx, nom in ((0.9, "Navigateur"), (6.5, "ESP32-S3")):
boite(ax, x0 + dx, 6.2, 1.9, 0.6, nom, couleur=GRIS if nom == "Navigateur" else BLEU, taille=8)
ax.plot([x0 + dx] * 2, [0.85, 5.85], color=GRIS, lw=1, ls=(0, (3, 3)))
# HTTP : une requête par échange
g, d = 1.2, 6.8
y = 5.4
for req, rep in (("GET /mesure", "200 OK « 21,5 °C »"), ("GET /mesure", "200 OK « 21,6 °C »"),
("GET /mesure", "200 OK « 21,6 °C »")):
fleche(ax, (g, y), (d, y), req, taille=7.3, decalage=0.05)
fleche(ax, (d, y - 0.55), (g, y - 0.55), rep, taille=7.3, pointille=True, decalage=0.05)
y -= 1.5
ax.text(4.0, 0.15, "le navigateur doit redemander\n(nouvelle requête à chaque fois)", ha="center",
fontsize=7.3, color=ORANGE)
# WebSocket : une connexion persistante
g, d = 9.2, 14.8
fleche(ax, (g, 5.4), (d, 5.4), "ouverture (handshake)", taille=7.3, decalage=0.05)
fleche(ax, (d, 4.85), (g, 4.85), "connexion acceptée", taille=7.3, pointille=True, decalage=0.05)
ax.add_patch(Rectangle((g, 0.75), d - g, 3.75, color=VERT, alpha=0.08))
for k, (sens, txt) in enumerate(((1, "21,5 °C"), (1, "21,6 °C"), (-1, "« rouge »"), (1, "21,7 °C"), (1, "21,7 °C"))):
yy = 4.1 - k * 0.7
a, b = (d, g) if sens == 1 else (g, d)
fleche(ax, (a, yy), (b, yy), txt, taille=7.3, couleur=VERT if sens == 1 else ROUGE, decalage=0.05)
ax.text(12.0, 0.15, "connexion ouverte en permanence :\nla carte envoie dès qu'une valeur change",
ha="center", fontsize=7.3, color=VERT)
return fig
FIGURES = [
Figure("m08-echanges-serveur", echanges_serveur),
Figure("m08-http-vs-websocket", http_vs_websocket),
]
+48
View File
@@ -0,0 +1,48 @@
"""Module 9 — Bluetooth Low Energy (BLE)."""
from outils import boite, fleche, sequence, toile
from style import BLEU, ENCRE, GRIS, ORANGE, VERT, Figure
def hierarchie_gatt():
fig, ax = toile(16, 7.0)
boite(ax, 5.6, 3.5, 10.8, 6.6, couleur=BLEU, fond="#f5f8fe")
ax.text(0.5, 6.4, "Serveur BLE — l'ESP32-S3", fontsize=9.5, fontweight="bold", color=BLEU)
ax.text(0.5, 6.0, 'BLEDevice::init("ESP32-S3-Formation")', fontsize=7.5, color=GRIS, family="monospace")
boite(ax, 5.6, 3.15, 10.0, 5.1, couleur=ORANGE)
ax.text(0.9, 5.25, "Service", fontsize=9, fontweight="bold", color=ORANGE)
ax.text(0.9, 4.9, "UUID 4fafc201-1fb5-459e-8fcc-c5c9c331914b", fontsize=7.5, color=ENCRE, family="monospace")
boite(ax, 5.6, 2.55, 9.2, 3.1, couleur=VERT)
ax.text(1.3, 3.75, "Caractéristique", fontsize=9, fontweight="bold", color=VERT)
ax.text(1.3, 3.4, "UUID beb5483e-36e1-4688-b7f5-ea07361b26a8", fontsize=7.5, color=ENCRE, family="monospace")
for i, (p, c) in enumerate((("READ", BLEU), ("WRITE", ORANGE), ("NOTIFY", VERT))):
boite(ax, 2.0 + i * 1.75, 2.6, 1.5, 0.55, p, couleur=c, taille=7.5, gras=True)
boite(ax, 8.4, 2.6, 2.6, 0.8, 'valeur : "rouge"', couleur=GRIS, taille=8)
ax.text(5.6, 1.4, "(un service peut contenir plusieurs caractéristiques)", ha="center",
fontsize=7.3, color=GRIS, style="italic")
boite(ax, 14.1, 3.5, 2.8, 2.0, "Client BLE\nsmartphone\n(nRF Connect)", couleur=GRIS, taille=8.5)
fleche(ax, (12.7, 3.0), (11.0, 3.0), "écrit", couleur=ORANGE, taille=7.5, decalage=0.08)
fleche(ax, (11.0, 2.2), (12.7, 2.2), "notifie", couleur=VERT, taille=7.5, decalage=0.08)
fleche(ax, (12.7, 4.1), (11.0, 4.1), "lit", couleur=BLEU, taille=7.5, decalage=0.08)
return fig
def sequence_ble():
acteurs = ["ESP32-S3 (serveur)", "Smartphone (client)"]
messages = [
("ESP32-S3 (serveur)", "Smartphone (client)", "annonce (advertising) « ESP32-S3-Formation »", True),
("Smartphone (client)", "ESP32-S3 (serveur)", "connexion"),
("Smartphone (client)", "ESP32-S3 (serveur)", "découverte des services et caractéristiques"),
("Smartphone (client)", "ESP32-S3 (serveur)", "WRITE « rouge »"),
("ESP32-S3 (serveur)", "ESP32-S3 (serveur)", "onWrite() → LED rouge"),
("ESP32-S3 (serveur)", "Smartphone (client)", "NOTIFY « 21.5 » (température)", True),
("ESP32-S3 (serveur)", "Smartphone (client)", "NOTIFY « 21.6 » … toutes les 5 s", True),
]
fig, ax = sequence(acteurs, messages, largeur_cm=15, pas_cm=0.85,
couleurs={"Smartphone (client)": GRIS})
return fig
FIGURES = [
Figure("m09-hierarchie-gatt", hierarchie_gatt),
Figure("m09-sequence-ble", sequence_ble),
]
+20 -12
View File
@@ -34,21 +34,26 @@ def boite(ax, x, y, l, h, texte="", couleur=BLEU, taille=9, gras=False, fond=Non
def fleche(ax, depart, arrivee, texte="", couleur=ENCRE, taille=8, double=False, pointille=False,
cote="haut", decalage=0.18, courbe=0):
"""Flèche entre deux points, avec étiquette optionnelle au milieu."""
cote="haut", decalage=0.18, courbe=0, fond=False):
"""Flèche entre deux points, avec étiquette optionnelle au milieu.
fond=True place l'étiquette sur fond blanc (utile quand elle croise des traits).
"""
ax.annotate("", xy=arrivee, xytext=depart,
arrowprops=dict(arrowstyle="<|-|>" if double else "-|>", color=couleur, lw=1.3,
linestyle="--" if pointille else "-", shrinkA=0, shrinkB=0,
mutation_scale=11, connectionstyle=f"arc3,rad={courbe}"))
if texte:
cadre = dict(boxstyle="square,pad=0.1", fc="white", ec="none") if fond else None
mx, my = (depart[0] + arrivee[0]) / 2, (depart[1] + arrivee[1]) / 2
vertical = abs(arrivee[0] - depart[0]) < abs(arrivee[1] - depart[1])
if vertical:
ax.text(mx + decalage, my, texte, ha="left", va="center", fontsize=taille, color=couleur)
ax.text(mx + decalage, my, texte, ha="left", va="center", fontsize=taille, color=couleur,
bbox=cadre)
else:
dy = decalage if cote == "haut" else -decalage
ax.text(mx, my + dy, texte, ha="center", va="bottom" if cote == "haut" else "top",
fontsize=taille, color=couleur)
fontsize=taille, color=couleur, bbox=cadre)
def chronogramme(voies, duree, largeur_cm=16, hauteur_voie_cm=1.5, graduations=None, unite="",
@@ -98,20 +103,23 @@ def sequence(acteurs, messages, largeur_cm=16, pas_cm=0.85, couleurs=None, notes
haut = hauteur - 0.6
for a, x in xs.items():
c = (couleurs or {}).get(a, BLEU)
boite(ax, x, haut, min(3.6, largeur_cm / len(acteurs) - 0.4), 0.8, a, couleur=c, taille=8.5, gras=True)
ax.plot([x, x], [0.3, haut - 0.4], color=GRIS, lw=1, ls=(0, (3, 3)))
largeur = min(largeur_cm / len(acteurs) - 0.3, 0.2 * len(a) + 0.9)
boite(ax, x, haut, largeur, 0.8, a, couleur=c, taille=8.5, gras=True)
ax.plot([x, x], [0.55, haut - 0.4], color=GRIS, lw=1, ls=(0, (3, 3)), zorder=0)
for k, msg in enumerate(messages):
src, dst, texte = msg[:3]
pointille = len(msg) > 3 and msg[3]
y = haut - 1.2 - k * pas_cm
if src == dst:
ax.annotate("", xy=(xs[src] + 0.05, y - 0.3), xytext=(xs[src] + 0.05, y),
arrowprops=dict(arrowstyle="-|>", color=ENCRE, lw=1.1,
connectionstyle="arc,angleA=0,angleB=0,armA=18,armB=18,rad=4",
mutation_scale=9))
ax.text(xs[src] + 0.75, y - 0.15, texte, fontsize=7.8, va="center", color=ENCRE)
# Boucle vers soi-même : petit rectangle ouvert à droite de la ligne de vie
x = xs[src]
ax.plot([x, x + 0.45, x + 0.45], [y + 0.12, y + 0.12, y - 0.22], color=ENCRE, lw=1.1)
fleche(ax, (x + 0.45, y - 0.22), (x + 0.02, y - 0.22))
ax.text(x + 0.6, y - 0.05, texte, fontsize=7.8, va="center", color=ENCRE,
bbox=dict(boxstyle="square,pad=0.1", fc="white", ec="none"))
continue
fleche(ax, (xs[src], y), (xs[dst], y), texte, pointille=pointille, taille=7.8, decalage=0.08)
fleche(ax, (xs[src], y), (xs[dst], y), texte, pointille=pointille, taille=7.8, decalage=0.08,
fond=True)
for k, acteur, texte in notes:
y = haut - 1.2 - k * pas_cm
ax.text(xs[acteur] + 0.2, y, texte, fontsize=7.5, color=GRIS, style="italic", va="center")
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 50 KiB