diff --git a/modules/00-installation.qmd b/modules/00-installation.qmd
index 434e9cf..56bf678 100644
--- a/modules/00-installation.qmd
+++ b/modules/00-installation.qmd
@@ -68,10 +68,14 @@ Une fois le paquet installé :
Si l'upload échoue avec un message « Failed to connect », maintenez le bouton BOOT appuyé, appuyez brièvement sur RESET, relâchez BOOT, puis relancez le téléversement. Ce geste force la carte en mode bootloader.
:::
+{#fig-m00-boot-reset width=90%}
+
## 5. Premier programme : Blink
Nous allons vérifier que toute la chaîne (code → compilation → upload → exécution) fonctionne, en faisant clignoter la LED intégrée.
+{#fig-m00-chaine-developpement}
+
```cpp
// Sur la plupart des DevKitC ESP32-S3, la LED simple (non RGB) est sur la broche 2.
// Vérifiez la sérigraphie de votre carte : elle peut varier.
diff --git a/modules/01-architecture.qmd b/modules/01-architecture.qmd
index d67f23a..a4ba8ba 100644
--- a/modules/01-architecture.qmd
+++ b/modules/01-architecture.qmd
@@ -17,6 +17,8 @@ L'ESP32-S3 va plus loin qu'un microcontrôleur classique (type ATmega328 de l'Ar
## 2. Architecture interne de l'ESP32-S3
+{#fig-m01-architecture}
+
### 2.1 Cœurs de calcul
- Double cœur Xtensa LX7 cadencé jusqu'à 240 MHz.
@@ -49,10 +51,14 @@ L'ESP32-S3 dispose d'un contrôleur USB OTG natif, permettant la programmation e
| Broches ADC | 10 bits, 5 entrées | 12 bits, jusqu'à 20 entrées |
| PWM | Limité (Timer matériel) | LEDC — jusqu'à 8 canaux configurables |
+{#fig-m01-uno-vs-s3}
+
::: {.callout-warning title="Point de vigilance : tension 3,3 V"}
Toutes les broches GPIO de l'ESP32-S3 fonctionnent en logique 3,3 V et ne tolèrent pas 5 V en entrée. Un signal 5 V (capteur, module tiers) doit être abaissé via un diviseur de tension ou un convertisseur de niveau logique avant d'être connecté à une broche.
:::
+{#fig-m01-diviseur-tension width=65%}
+
## 4. Lire le brochage (pinout) de la carte
Avant de câbler quoi que ce soit, il est indispensable de repérer sur votre carte :
diff --git a/modules/02-gpio-neopixel.qmd b/modules/02-gpio-neopixel.qmd
index 14b634a..8129602 100644
--- a/modules/02-gpio-neopixel.qmd
+++ b/modules/02-gpio-neopixel.qmd
@@ -35,6 +35,8 @@ Une entrée numérique non connectée (« flottante ») peut lire un état aléa
- `INPUT_PULLUP` : la broche est tirée à 3,3 V par défaut (état HIGH) ; le bouton doit relier la broche à GND pour donner un état LOW à l'appui.
- `INPUT_PULLDOWN` : disponible sur l'ESP32-S3 (contrairement à l'AVR classique) — la broche est tirée à 0 V par défaut.
+{#fig-m02-pull-up width=70%}
+
```cpp
#define BUTTON_PIN 5
@@ -56,10 +58,14 @@ void loop() {
Un bouton mécanique génère plusieurs transitions HIGH/LOW parasites en quelques millisecondes lors de l'appui. Un simple `delay(50)` après lecture suffit pour un usage pédagogique ; pour un projet robuste, on mesure le temps écoulé avec `millis()` plutôt que d'utiliser `delay()` (voir Module 6 sur les interruptions et timers).
:::
+{#fig-m02-rebonds}
+
## 3. Exercice guidé : chenillard de LED
Câblez 4 LED (avec résistances de 220 Ω) sur 4 GPIO libres, puis reproduisez un effet de chenillard :
+{#fig-m02-chenillard width=75%}
+
```cpp
const int leds[] = {4, 5, 6, 7};
const int nbLeds = 4;
@@ -87,6 +93,8 @@ Contrairement à une LED classique qui ne prend que deux états (allumé/éteint
Le protocole WS2812 impose un timing très précis (de l'ordre de la centaine de nanosecondes) pour distinguer un bit 0 d'un bit 1. Le générer « à la main » avec digitalWrite serait peu fiable ; on utilise donc une bibliothèque dédiée qui exploite les ressources matérielles du microcontrôleur pour respecter ce timing.
+{#fig-m02-ws2812}
+
### 4.2 Installation de la bibliothèque
- Dans l'IDE : Croquis \> Inclure une bibliothèque \> Gérer les bibliothèques.
diff --git a/schemas/m00_installation.py b/schemas/m00_installation.py
new file mode 100644
index 0000000..85b5a76
--- /dev/null
+++ b/schemas/m00_installation.py
@@ -0,0 +1,53 @@
+"""Module 0 — Prérequis & installation."""
+from outils import boite, chronogramme, fleche, toile
+from style import BLEU, ENCRE, GRIS, ORANGE, VERT, Figure
+
+
+def chaine_developpement():
+ fig, ax = toile(16, 5.6)
+ # Zones ordinateur / carte
+ boite(ax, 5.0, 3.0, 9.4, 4.6, couleur=GRIS, fond="#f9fafb")
+ boite(ax, 13.2, 3.0, 4.6, 4.6, couleur=BLEU, fond="#f5f8fe")
+ ax.text(0.6, 4.8, "Ordinateur — Arduino IDE", fontsize=9, color=GRIS, fontweight="bold")
+ ax.text(11.2, 4.8, "Carte ESP32-S3", fontsize=9, color=BLEU, fontweight="bold")
+ code = boite(ax, 2.0, 3.3, 2.6, 1.4, "Croquis\n(.ino)", couleur=BLEU)
+ comp = boite(ax, 5.6, 3.3, 2.6, 1.4, "Compilation\n✓ Vérifier", couleur=BLEU)
+ moni = boite(ax, 4.2, 1.4, 5.4, 0.9, "Moniteur série — 115200 bauds", couleur=VERT, taille=8.5)
+ flash = boite(ax, 13.2, 3.3, 3.2, 1.4, "Mémoire Flash\n(programme)", couleur=ORANGE)
+ cpu = boite(ax, 13.2, 1.4, 3.2, 0.9, "Exécution", couleur=ORANGE)
+ fleche(ax, code["d"], comp["g"])
+ fleche(ax, comp["d"], flash["g"], "Téléverser →\n(câble USB)", taille=8)
+ fleche(ax, flash["b"], cpu["h"])
+ fleche(ax, cpu["g"], moni["d"], "Serial.println()", couleur=VERT, taille=8)
+ ax.text(8.0, 0.1, "Le même câble USB sert au téléversement et au moniteur série (USB CDC).",
+ ha="center", fontsize=7.5, color=GRIS, style="italic")
+ return fig
+
+
+def boot_reset():
+ voies = [
+ ("BOOT", [(0, 1), (1, 0), (4, 1)]),
+ ("RESET", [(0, 1), (2, 0), (2.6, 1)]),
+ ]
+ fig, ax, y = chronogramme(voies, 6, largeur_cm=15, hauteur_voie_cm=1.6,
+ couleurs={"RESET": ORANGE})
+ ax.set_xticks([])
+ ax.spines["bottom"].set_visible(False)
+ for i in range(2):
+ ax.text(6.05, y(i) + 1, "relâché", fontsize=7, color=GRIS, va="center")
+ ax.text(6.05, y(i), "appuyé", fontsize=7, color=GRIS, va="center")
+ etapes = [(1, "① maintenir BOOT"), (2.3, "② appui bref\nsur RESET"), (4, "③ relâcher BOOT")]
+ for t, txt in etapes:
+ ax.axvline(t, color=GRIS, lw=0.8, ls=":")
+ ax.text(t, 3.05, txt, ha="center", va="bottom", fontsize=8, color=ENCRE)
+ ax.text(5.0, -0.75, "→ la carte démarre en mode téléchargement", ha="center", fontsize=8.5,
+ color=VERT, fontweight="bold")
+ ax.set_ylim(-1.1, 4.1)
+ fig.tight_layout()
+ return fig
+
+
+FIGURES = [
+ Figure("m00-chaine-developpement", chaine_developpement),
+ Figure("m00-boot-reset", boot_reset),
+]
diff --git a/schemas/m01_architecture.py b/schemas/m01_architecture.py
new file mode 100644
index 0000000..9649675
--- /dev/null
+++ b/schemas/m01_architecture.py
@@ -0,0 +1,85 @@
+"""Module 1 — Découverte du microcontrôleur ESP32-S3."""
+import matplotlib.pyplot as plt
+import schemdraw
+import schemdraw.elements as elm
+
+from outils import boite, fleche, toile
+from style import BLEU, CM, ENCRE, GRIS, ORANGE, VERT, Figure
+
+schemdraw.use("matplotlib")
+
+
+def architecture():
+ fig, ax = toile(16, 8.2)
+ boite(ax, 6.6, 4.2, 12.6, 7.6, couleur=BLEU, fond="#f5f8fe")
+ ax.text(0.6, 7.6, "ESP32-S3 (puce)", fontsize=10, color=BLEU, fontweight="bold")
+ # Cœurs et mémoire
+ boite(ax, 2.6, 6.2, 3.8, 1.0, "Cœur 0\nXtensa LX7", couleur=BLEU, taille=8.5)
+ boite(ax, 2.6, 5.0, 3.8, 1.0, "Cœur 1\nXtensa LX7", couleur=BLEU, taille=8.5)
+ ax.text(2.6, 4.2, "240 MHz max · FreeRTOS", ha="center", fontsize=7.5, color=GRIS)
+ sram = boite(ax, 6.6, 5.6, 3.0, 2.2, "SRAM interne\n≈ 512 Ko\n(données +\ninstructions)", couleur=VERT, taille=8.5)
+ # Radio
+ boite(ax, 10.6, 6.2, 3.6, 1.0, "Wi-Fi 802.11 b/g/n\n2,4 GHz", couleur=ORANGE, taille=8)
+ boite(ax, 10.6, 5.0, 3.6, 1.0, "Bluetooth 5 LE\n(pas de Classic)", couleur=ORANGE, taille=8)
+ # Périphériques
+ periph = ["GPIO", "ADC 12 bits", "LEDC (PWM)", "UART", "I2C", "SPI", "Timers", "USB OTG"]
+ for i, p in enumerate(periph):
+ x = 1.35 + (i % 4) * 2.85
+ yy = 2.55 if i < 4 else 1.35
+ boite(ax, x, yy, 2.55, 0.9, p, couleur=GRIS, taille=8.5)
+ ax.text(0.6, 3.25, "Périphériques intégrés", fontsize=8, color=GRIS, fontweight="bold")
+ # Bus interne
+ ax.plot([0.7, 12.5], [3.8, 3.8], color=BLEU, lw=2.2, alpha=0.5)
+ # Externe
+ flash = boite(ax, 14.75, 5.6, 2.45, 2.2, "Flash\nexterne\n8–16 Mo\n(programme,\nfichiers)", couleur=VERT, taille=7.5)
+ psram = boite(ax, 14.7, 2.9, 2.3, 1.2, "PSRAM\n(option)", couleur=VERT, taille=8, alpha=0.8)
+ ax.text(14.7, 7.15, "sur le module", ha="center", fontsize=7.5, color=GRIS, style="italic")
+ fleche(ax, (12.9, 5.6), flash["g"], double=True)
+ fleche(ax, (12.9, 2.9), psram["g"], double=True)
+ return fig
+
+
+def uno_vs_s3():
+ fig, ax = plt.subplots(figsize=(16 * CM, 6.2 * CM))
+ criteres = ["Fréquence (MHz)", "RAM (Ko)", "Flash (Ko)"]
+ uno = [16, 2, 32]
+ s3 = [240, 512, 8192]
+ y = range(len(criteres))
+ ax.barh([i + 0.2 for i in y], uno, height=0.38, color=GRIS, label="Arduino Uno (ATmega328)")
+ ax.barh([i - 0.2 for i in y], s3, height=0.38, color=BLEU, label="ESP32-S3")
+ ax.set_xscale("log")
+ ax.set_xlim(1, 300000)
+ ax.set_yticks(list(y), criteres)
+ ax.invert_yaxis()
+ for i, (u, s) in enumerate(zip(uno, s3)):
+ ax.text(u * 1.15, i + 0.2, f"{u:g}", va="center", fontsize=8, color=ENCRE)
+ lib = f"{s:g}" if s < 8000 else "8192 (8 Mo)"
+ ax.text(s * 1.15, i - 0.2, f"{lib} × {s // u}", va="center", fontsize=8, color=BLEU,
+ fontweight="bold")
+ ax.set_xlabel("échelle logarithmique : chaque graduation = × 10")
+ ax.grid(axis="y", visible=False)
+ ax.legend(loc="lower center", bbox_to_anchor=(0.5, 1.0), ncol=2, fontsize=8, frameon=False)
+ fig.tight_layout()
+ return fig
+
+
+def diviseur_tension():
+ d = schemdraw.Drawing(show=False)
+ d.config(fontsize=11, unit=2.6, color=ENCRE)
+ d += elm.Dot(open=True).label("Signal 5 V\n(module tiers)", loc="left")
+ d += elm.Resistor().right().label("R1 = 10 kΩ").color(ORANGE)
+ d += (noeud := elm.Dot())
+ d += elm.Line().right().length(2.2)
+ d += elm.Dot(open=True).label("GPIO\n(3,3 V max)", loc="right").color(BLEU)
+ d += elm.Resistor().down().at(noeud.center).label("R2 = 20 kΩ", loc="bottom").color(ORANGE)
+ d += elm.Ground()
+ d += elm.Label().at((noeud.center[0] + 1.2, noeud.center[1] - 2.2)).label(
+ r"$V_{GPIO} = 5\,V \times \frac{R2}{R1 + R2} \approx 3{,}3\,V$", fontsize=11, color=VERT)
+ return d.draw(show=False)
+
+
+FIGURES = [
+ Figure("m01-architecture", architecture),
+ Figure("m01-uno-vs-s3", uno_vs_s3),
+ Figure("m01-diviseur-tension", diviseur_tension),
+]
diff --git a/schemas/m02_gpio_neopixel.py b/schemas/m02_gpio_neopixel.py
new file mode 100644
index 0000000..91584f2
--- /dev/null
+++ b/schemas/m02_gpio_neopixel.py
@@ -0,0 +1,121 @@
+"""Module 2 — Entrées/sorties numériques & LED RGB NeoPixel."""
+import matplotlib.pyplot as plt
+import numpy as np
+import schemdraw
+import schemdraw.elements as elm
+
+from outils import chronogramme
+from style import BLEU, CM, ENCRE, GRIS, ORANGE, ROUGE, VERT, Figure
+
+schemdraw.use("matplotlib")
+
+
+def pull_up():
+ d = schemdraw.Drawing(show=False)
+ d.config(fontsize=10.5, unit=2.4, color=ENCRE)
+ d += (vdd := elm.Vdd().label("3V3"))
+ d += (r := elm.Resistor().down().label("pull-up interne\n≈ 45 kΩ", loc="bottom").color(BLEU))
+ d += (n := elm.Dot())
+ d += (lecture := elm.Line().left().length(2.6).label("vers digitalRead()", loc="top", fontsize=9).color(BLEU))
+ d += elm.Line().right().at(n.center).length(2.8)
+ d += (gpio := elm.Dot(open=True).label("GPIO5", loc="top"))
+ d += elm.Line().right().length(1.2)
+ d += elm.Button().down().label("bouton", loc="bottom").color(ORANGE)
+ d += elm.Ground()
+ d += elm.EncircleBox([vdd, r, n, lecture, gpio], padx=0.3, pady=0.5).linestyle("--").color(GRIS)
+ d += elm.Label().at((n.center[0] + 1.6, vdd.end[1] + 0.2)).label("à l'intérieur\nde l'ESP32-S3", color=GRIS, fontsize=9)
+ d += elm.Label().at((gpio.center[0] + 3.6, n.center[1] - 2.0)).label(
+ "relâché → HIGH (3,3 V)\nappuyé → LOW (0 V)", fontsize=10, color=VERT)
+ return d.draw(show=False)
+
+
+def rebonds():
+ t = [0]
+ niv = [1]
+ # Appui avec rebonds autour de 40 ms, relâchement avec rebonds autour de 140 ms
+ for a, b in ((40, 0), (41.2, 1), (42, 0), (43.5, 1), (44.1, 0), (140, 1), (140.8, 0), (141.9, 1), (142.5, 0), (143.2, 1)):
+ t.append(a)
+ niv.append(b)
+ voies = [("broche (brut)", list(zip(t, niv))),
+ ("état retenu", [(0, 1), (54, 0), (153, 1)])]
+ fig, ax, y = chronogramme(voies, 200, largeur_cm=16, hauteur_voie_cm=1.7,
+ graduations=range(0, 201, 25), unite="temps (ms)",
+ couleurs={"état retenu": VERT})
+ for x0, x1, txt in ((40, 44.1, "rebonds\n(quelques ms)"), (140, 143.2, "rebonds")):
+ ax.axvspan(x0 - 1, x1 + 1, color=ORANGE, alpha=0.18)
+ ax.text((x0 + x1) / 2, y(0) + 1.25, txt, ha="center", fontsize=7.5, color=ORANGE)
+ ax.annotate("", xy=(54, y(1) + 0.5), xytext=(44.1, y(1) + 0.5),
+ arrowprops=dict(arrowstyle="<->", color=ENCRE, lw=0.9))
+ ax.text(49, y(1) + 0.62, "attente", ha="center", fontsize=7, color=ENCRE)
+ ax.text(97, y(1) + 0.3, "un seul appui détecté", ha="center", fontsize=8, color=VERT, fontweight="bold")
+ ax.set_ylim(-0.5, 3.9)
+ fig.tight_layout()
+ return fig
+
+
+def chenillard():
+ d = schemdraw.Drawing(show=False)
+ d.config(fontsize=10.5, unit=2.2, color=ENCRE)
+ pins = [elm.IcPin(name=f"GPIO{g}", side="right", anchorname=f"P{g}") for g in (7, 6, 5, 4)]
+ ic = elm.Ic(pins=pins, size=(3.2, 5.4), pinfontsize=10).label("ESP32-S3", loc="center", fontsize=12).color(BLEU)
+ d += ic
+ couleurs = {4: ROUGE, 5: VERT, 6: BLEU, 7: ORANGE}
+ fins = []
+ for g in (4, 5, 6, 7):
+ d += elm.Line().right().at(getattr(ic, f"P{g}")).length(0.6)
+ d += elm.Resistor().right().label("220 Ω", fontsize=9)
+ d += elm.LED().right().color(couleurs[g])
+ d += elm.Line().right().length(0.6)
+ fins.append(d.here)
+ d += elm.Line().at(fins[0]).to(fins[-1])
+ d += elm.Line().down().at(fins[-1]).length(0.6)
+ d += elm.Ground()
+ d += elm.Label().at((fins[0][0] + 0.4, (fins[0][1] + fins[-1][1]) / 2)).label("GND\ncommun", loc="right", fontsize=9, color=GRIS)
+ return d.draw(show=False)
+
+
+def ws2812():
+ fig = plt.figure(figsize=(16 * CM, 8.6 * CM))
+ # (a) trame 24 bits
+ ax1 = fig.add_axes([0.04, 0.62, 0.92, 0.3])
+ ax1.axis("off")
+ ax1.set_xlim(0, 24)
+ ax1.set_ylim(0, 1.6)
+ for k, (nom, c) in enumerate((("G", VERT), ("R", ROUGE), ("B", BLEU))):
+ for b in range(8):
+ x = k * 8 + b
+ ax1.add_patch(plt.Rectangle((x, 0.3), 1, 0.8, facecolor=c, alpha=0.18 + 0.06 * (7 - b) / 7,
+ edgecolor=c, lw=1))
+ ax1.text(x + 0.5, 0.7, f"{nom}{7 - b}", ha="center", va="center", fontsize=6.5, color=ENCRE)
+ ax1.text(k * 8 + 4, 1.25, {"G": "Vert (8 bits)", "R": "Rouge (8 bits)", "B": "Bleu (8 bits)"}[nom],
+ ha="center", fontsize=8.5, color=c, fontweight="bold")
+ ax1.text(0, 0.02, "(a) Une couleur = 24 bits, envoyés dans l'ordre G-R-B (d'où NEO_GRB), bit de poids fort en premier",
+ fontsize=8, color=GRIS)
+ # (b) codage d'un bit
+ ax2 = fig.add_axes([0.08, 0.12, 0.88, 0.36])
+ for x0, th, tl, nom in ((0, 0.4, 0.85, "bit 0"), (1.8, 0.8, 0.45, "bit 1")):
+ xs = [x0, x0, x0 + th, x0 + th, x0 + th + tl]
+ ys = [0, 1, 1, 0, 0]
+ ax2.plot(xs, ys, color=BLEU, lw=1.8)
+ ax2.fill_between([x0, x0 + th], 0, 1, color=BLEU, alpha=0.12)
+ ax2.text(x0 + (th + tl) / 2, 1.32, nom, ha="center", fontsize=9, fontweight="bold", color=ENCRE)
+ ax2.annotate("", xy=(x0, 1.1), xytext=(x0 + th, 1.1), arrowprops=dict(arrowstyle="<->", lw=0.8))
+ ax2.text(x0 + th / 2, 1.14, f"{th} µs".replace(".", ","), ha="center", va="bottom", fontsize=7.5)
+ ax2.annotate("", xy=(x0 + th, -0.12), xytext=(x0 + th + tl, -0.12), arrowprops=dict(arrowstyle="<->", lw=0.8))
+ ax2.text(x0 + th + tl / 2, -0.2, f"{tl} µs".replace(".", ","), ha="center", va="top", fontsize=7.5)
+ ax2.text(3.4, 0.55, "période d'un bit : 1,25 µs (800 kHz)\nla différence se joue à ~400 ns près :\n"
+ "impossible avec digitalWrite(),\nla bibliothèque utilise le matériel (RMT)",
+ fontsize=8, color=ENCRE, va="center")
+ ax2.set_xlim(-0.1, 6.2)
+ ax2.set_ylim(-0.5, 1.6)
+ ax2.axis("off")
+ fig.text(0.04, 0.03, "(b) Codage d'un bit : seule la durée de l'état haut change", fontsize=8, color=GRIS)
+ return fig
+
+
+FIGURES = [
+ Figure("m02-pull-up", pull_up),
+ Figure("m02-rebonds", rebonds),
+ Figure("m02-chenillard", chenillard),
+ Figure("m02-ws2812", ws2812),
+]
diff --git a/schemas/outils.py b/schemas/outils.py
new file mode 100644
index 0000000..f4e2830
--- /dev/null
+++ b/schemas/outils.py
@@ -0,0 +1,129 @@
+"""Briques graphiques communes : toile de dessin, boîtes, flèches, chronogrammes, séquences."""
+import matplotlib.pyplot as plt
+from matplotlib.patches import FancyBboxPatch
+
+from style import BLEU, CM, ENCRE, GRIS, GRIS_CLAIR
+
+CLAIR = { # fonds pâles associés aux couleurs de la charte
+ BLEU: "#e8f0fe", "#e07a1f": "#fdf0e3", "#2e8b57": "#e6f4ea",
+ "#c0392b": "#fce8e6", GRIS: "#f3f4f6",
+}
+
+
+def toile(largeur_cm, hauteur_cm, xmax=None, ymax=None):
+ """Figure sans axes, en coordonnées « cm » (1 unité = 1 cm à l'échelle d'origine)."""
+ fig, ax = plt.subplots(figsize=(largeur_cm * CM, hauteur_cm * CM))
+ ax.set_xlim(0, xmax or largeur_cm)
+ ax.set_ylim(0, ymax or hauteur_cm)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ return fig, ax
+
+
+def boite(ax, x, y, l, h, texte="", couleur=BLEU, taille=9, gras=False, fond=None, style="round", alpha=1):
+ """Boîte arrondie centrée sur (x, y). Renvoie un dict des points d'ancrage."""
+ fond = fond if fond is not None else CLAIR.get(couleur, "white")
+ ax.add_patch(FancyBboxPatch((x - l / 2, y - h / 2), l, h,
+ boxstyle=f"{style},pad=0,rounding_size=0.15" if style == "round" else style,
+ facecolor=fond, edgecolor=couleur, linewidth=1.4, alpha=alpha))
+ if texte:
+ ax.text(x, y, texte, ha="center", va="center", fontsize=taille, color=ENCRE,
+ fontweight="bold" if gras else "normal", linespacing=1.3)
+ return {"c": (x, y), "g": (x - l / 2, y), "d": (x + l / 2, y),
+ "h": (x, y + h / 2), "b": (x, y - h / 2)}
+
+
+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."""
+ 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:
+ 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)
+ 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)
+
+
+def chronogramme(voies, duree, largeur_cm=16, hauteur_voie_cm=1.5, graduations=None, unite="",
+ couleurs=None):
+ """Chronogramme logique.
+
+ voies : liste de (nom, [(t, niveau), ...]) — niveau 0/1, maintenu jusqu'au point suivant.
+ Renvoie (fig, ax, y_voie) où y_voie(i) donne l'ordonnée de base de la voie i.
+ """
+ n = len(voies)
+ fig, ax = plt.subplots(figsize=(largeur_cm * CM, (n * hauteur_voie_cm + 1.2) * CM))
+ ax.set_xlim(-duree * 0.02, duree * 1.01)
+ ax.set_ylim(-0.6, n * 1.6)
+ ax.spines[["left", "top", "right"]].set_visible(False)
+ ax.set_yticks([])
+ ax.grid(axis="y", visible=False)
+ if graduations is not None:
+ ax.set_xticks(graduations)
+ if unite:
+ ax.set_xlabel(unite)
+ y_voie = lambda i: (n - 1 - i) * 1.6
+ for i, (nom, points) in enumerate(voies):
+ base = y_voie(i)
+ xs, ys = [], []
+ for k, (t, niv) in enumerate(points):
+ fin = points[k + 1][0] if k + 1 < len(points) else duree
+ xs += [t, fin]
+ ys += [base + niv, base + niv]
+ c = (couleurs or {}).get(nom, BLEU)
+ ax.plot(xs, ys, color=c, lw=1.8, solid_joinstyle="miter")
+ ax.fill_between(xs, base, ys, color=c, alpha=0.08)
+ ax.text(-duree * 0.025, base + 0.5, nom, ha="right", va="center", fontsize=9,
+ color=ENCRE, fontweight="bold")
+ return fig, ax, y_voie
+
+
+def sequence(acteurs, messages, largeur_cm=16, pas_cm=0.85, couleurs=None, notes=()):
+ """Diagramme de séquence.
+
+ acteurs : liste de noms. messages : liste de (source, cible, texte[, pointillé]).
+ notes : liste de (index_message, acteur, texte) affichées à droite de la ligne de vie.
+ """
+ n = len(messages)
+ hauteur = n * pas_cm + 2.2
+ fig, ax = toile(largeur_cm, hauteur)
+ xs = {a: largeur_cm * (i + 0.5) / len(acteurs) for i, a in enumerate(acteurs)}
+ 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)))
+ 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)
+ continue
+ fleche(ax, (xs[src], y), (xs[dst], y), texte, pointille=pointille, taille=7.8, decalage=0.08)
+ 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")
+ return fig, ax
+
+
+def legende_couleurs(ax, elements, x, y, taille=7.5):
+ """Petite légende manuelle : elements = [(couleur, texte), ...]."""
+ for i, (c, t) in enumerate(elements):
+ ax.add_patch(plt.Rectangle((x, y - i * 0.45 - 0.12), 0.35, 0.25, color=c))
+ ax.text(x + 0.5, y - i * 0.45, t, fontsize=taille, va="center", color=ENCRE)
+
+
+__all__ = ["toile", "boite", "fleche", "chronogramme", "sequence", "legende_couleurs",
+ "CLAIR", "GRIS_CLAIR"]
diff --git a/schemas/png/m00-boot-reset.png b/schemas/png/m00-boot-reset.png
new file mode 100644
index 0000000..9ee890e
Binary files /dev/null and b/schemas/png/m00-boot-reset.png differ
diff --git a/schemas/png/m00-chaine-developpement.png b/schemas/png/m00-chaine-developpement.png
new file mode 100644
index 0000000..f673c02
Binary files /dev/null and b/schemas/png/m00-chaine-developpement.png differ
diff --git a/schemas/png/m01-architecture.png b/schemas/png/m01-architecture.png
new file mode 100644
index 0000000..1e273dd
Binary files /dev/null and b/schemas/png/m01-architecture.png differ
diff --git a/schemas/png/m01-diviseur-tension.png b/schemas/png/m01-diviseur-tension.png
new file mode 100644
index 0000000..2d6c592
Binary files /dev/null and b/schemas/png/m01-diviseur-tension.png differ
diff --git a/schemas/png/m01-uno-vs-s3.png b/schemas/png/m01-uno-vs-s3.png
new file mode 100644
index 0000000..a79a6b3
Binary files /dev/null and b/schemas/png/m01-uno-vs-s3.png differ
diff --git a/schemas/png/m02-chenillard.png b/schemas/png/m02-chenillard.png
new file mode 100644
index 0000000..8fc4985
Binary files /dev/null and b/schemas/png/m02-chenillard.png differ
diff --git a/schemas/png/m02-pull-up.png b/schemas/png/m02-pull-up.png
new file mode 100644
index 0000000..d77c72e
Binary files /dev/null and b/schemas/png/m02-pull-up.png differ
diff --git a/schemas/png/m02-rebonds.png b/schemas/png/m02-rebonds.png
new file mode 100644
index 0000000..6acacae
Binary files /dev/null and b/schemas/png/m02-rebonds.png differ
diff --git a/schemas/png/m02-ws2812.png b/schemas/png/m02-ws2812.png
new file mode 100644
index 0000000..e2e9e1a
Binary files /dev/null and b/schemas/png/m02-ws2812.png differ
diff --git a/schemas/svg/m00-boot-reset.svg b/schemas/svg/m00-boot-reset.svg
new file mode 100644
index 0000000..8186397
--- /dev/null
+++ b/schemas/svg/m00-boot-reset.svg
@@ -0,0 +1,1492 @@
+
+
+
diff --git a/schemas/svg/m00-chaine-developpement.svg b/schemas/svg/m00-chaine-developpement.svg
new file mode 100644
index 0000000..cd43b89
--- /dev/null
+++ b/schemas/svg/m00-chaine-developpement.svg
@@ -0,0 +1,2427 @@
+
+
+
diff --git a/schemas/svg/m01-architecture.svg b/schemas/svg/m01-architecture.svg
new file mode 100644
index 0000000..085eb67
--- /dev/null
+++ b/schemas/svg/m01-architecture.svg
@@ -0,0 +1,2619 @@
+
+
+
diff --git a/schemas/svg/m01-diviseur-tension.svg b/schemas/svg/m01-diviseur-tension.svg
new file mode 100644
index 0000000..dd0f251
--- /dev/null
+++ b/schemas/svg/m01-diviseur-tension.svg
@@ -0,0 +1,1110 @@
+
+
+
diff --git a/schemas/svg/m01-uno-vs-s3.svg b/schemas/svg/m01-uno-vs-s3.svg
new file mode 100644
index 0000000..60ce6c4
--- /dev/null
+++ b/schemas/svg/m01-uno-vs-s3.svg
@@ -0,0 +1,1987 @@
+
+
+
diff --git a/schemas/svg/m02-chenillard.svg b/schemas/svg/m02-chenillard.svg
new file mode 100644
index 0000000..50966c1
--- /dev/null
+++ b/schemas/svg/m02-chenillard.svg
@@ -0,0 +1,940 @@
+
+
+
diff --git a/schemas/svg/m02-pull-up.svg b/schemas/svg/m02-pull-up.svg
new file mode 100644
index 0000000..5370617
--- /dev/null
+++ b/schemas/svg/m02-pull-up.svg
@@ -0,0 +1,1712 @@
+
+
+
diff --git a/schemas/svg/m02-rebonds.svg b/schemas/svg/m02-rebonds.svg
new file mode 100644
index 0000000..c3daa83
--- /dev/null
+++ b/schemas/svg/m02-rebonds.svg
@@ -0,0 +1,1367 @@
+
+
+
diff --git a/schemas/svg/m02-ws2812.svg b/schemas/svg/m02-ws2812.svg
new file mode 100644
index 0000000..4473ae2
--- /dev/null
+++ b/schemas/svg/m02-ws2812.svg
@@ -0,0 +1,2633 @@
+
+
+