Compare commits

..
Author SHA1 Message Date
ArtemiaandClaude Opus 5.5 170588dc2c Corrige le texte qui débordait des boîtes dans les schémas, et l'en-tête des PDF
toile() laissait les marges par défaut de matplotlib : une unité valait ~0,78 cm au lieu de 1 cm, les boîtes étaient donc trop petites pour leur texte. Les axes occupent désormais toute la figure, et boite() réduit la police si le texte dépasse encore (ajuster_texte). En-tête et pied de page : grille et align() à la place de #h(1fr), neutralisé par le préambule de Quarto (titre renvoyé à la ligne, numéro de page à gauche).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:25:29 +02:00
ArtemiaandClaude Opus 5.5 379cd03bc5 Illustre les modules 10 à 12 et la synthèse (7 figures)
M10 : consommation par mode, profil de courant en deep sleep. M11 : publish/subscribe, arborescence des topics. M12 : démarche de projet, structure multi-fichiers. Synthèse : carte du parcours. Figure du chenillard (M2) déplacée après son code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:24:43 +02:00
ArtemiaandClaude Opus 5.5 f4a80f4d5b 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>
2026-10-18 15:20:12 +02:00
ArtemiaandClaude Opus 5.5 372c5a3e1e Illustre les modules 4 à 6 (9 figures)
M4 : trame UART 8N1, câblage série croisé. M5 : bus I2C, trame I2C, bus SPI. M6 : tâches millis(), déroulé d'une ISR, modes de déclenchement, encodeur rotatif.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:43 +02:00
ArtemiaandClaude Opus 5.5 499d11402b Corrige le module 4 : bloc de code converti par erreur en texte
Dans le .odt, l'encadré « Réassignation des broches UART » et l'exemple Serial2 partageaient le même tableau ; le code était devenu du texte (lignes #define interprétées comme titres). Régénéré depuis l'original (tag avant-quarto), contenu vérifié identique.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:00 +02:00
ArtemiaandClaude Opus 5.5 30d0e356a6 Illustre les modules 0 à 2 (9 figures)
Bibliothèque commune schemas/outils.py (boîtes, flèches, chronogrammes, séquences). M0 : chaîne de développement, séquence BOOT/RESET. M1 : architecture, comparatif Uno/ESP32-S3, diviseur de tension. M2 : pull-up, rebonds, chenillard, protocole WS2812.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:11:18 +02:00
ArtemiaandClaude Opus 5.5 d933e348c0 Retire les .odt et l'insertion XML, désormais remplacés par Quarto
Les supports .odt restent récupérables via le tag avant-quarto. Les figures ne déclarent plus que leur identifiant et leur fonction de dessin (légende et emplacement dans le .qmd). Génération SVG reproductible (sans date, identifiants stables) pour des diffs git propres. README : mode d'emploi Quarto et schémas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:04:25 +02:00
ArtemiaandClaude Opus 5.5 85852fcc2f Convertit les 13 autres supports en .qmd
Modules 00 à 12 et synthèse matériel convertis avec le même outil que le pilote. Aucun mot perdu (comparaison automatique avec chaque .odt). Tableaux : largeurs de colonnes proportionnelles au contenu, texte non justifié.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:03:13 +02:00
ArtemiaandClaude Opus 5.5 ff1628efeb Passe à Quarto : projet, gabarit PDF (Typst) et module 3 en .qmd
Le Markdown remplace le .odt binaire comme source : modifications lisibles dans git, PDF produit par 'quarto render'. Le gabarit reprend la charte des supports (titres bleus, encadrés, code coloré, en-tête et pied de page). Conversion vérifiée : 865 mots, aucun écart avec le .odt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:55:16 +02:00
117 changed files with 66485 additions and 394 deletions
+8
View File
@@ -22,3 +22,11 @@ secrets.h
# Environnement Python
.venv/
__pycache__/
/.quarto/
**/*.quarto_ipynb
# Sorties Quarto (PDF générés)
_output/
.quarto/
modules/*.typ
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+59 -16
View File
@@ -1,20 +1,63 @@
# Formation Arduino (ESP32)
# Formation Arduino (ESP32-S3)
Support de formation en 13 modules (00 à 12) plus une synthèse du matériel, au format OpenDocument (.odt).
Support de formation en 13 modules (00 à 12) plus une synthèse du matériel.
Les sources sont en Markdown [Quarto](https://quarto.org) (`modules/*.qmd`) et produisent des PDF.
| Fichier | Sujet |
|---|---|
| Module_00_Installation | Installation de l'environnement |
| Module_01_Architecture | Architecture |
| Module_02_GPIO_NeoPixel | GPIO et NeoPixel |
| Module_03_PWM_ADC | PWM et ADC |
| Module_04_Serie_Debug | Liaison série et débogage |
| Module_05_I2C_SPI | I2C et SPI |
| Module_06_Interruptions_Timers | Interruptions et timers |
| Module_07_WiFi | WiFi |
| Module_08_ServeurWeb | Serveur web |
| Module_09_BLE | Bluetooth Low Energy |
| Module_10_Energie | Gestion de l'énergie |
| Module_11_MQTT | MQTT |
| Module_12_ProjetFinal | Projet final |
| Synthese_Formation_Materiel | Synthèse du matériel |
| `00-installation` | Installation de l'environnement |
| `01-architecture` | Découverte du microcontrôleur ESP32-S3 |
| `02-gpio-neopixel` | Entrées/sorties numériques et LED NeoPixel |
| `03-pwm-adc` | Signaux analogiques : PWM et ADC |
| `04-serie-debug` | Communication série et débogage |
| `05-i2c-spi` | Bus I2C et SPI |
| `06-interruptions-timers` | Interruptions matérielles et timers |
| `07-wifi` | Wi-Fi : station, point d'accès, client HTTP |
| `08-serveurweb` | Serveur web embarqué |
| `09-ble` | Bluetooth Low Energy |
| `10-energie` | Gestion de l'énergie |
| `11-mqtt` | IoT et MQTT |
| `12-projetfinal` | Projet final |
| `synthese-materiel` | Synthèse et liste de matériel |
## Organisation
```
modules/ sources des supports (.qmd)
modele/ gabarit PDF (Typst) et thème de coloration du code
schemas/ schémas décrits en Python (schemdraw + matplotlib), rendus en svg/ et png/
_quarto.yml configuration du projet
_output/ PDF générés (non versionnés)
```
## Produire les PDF
Prérequis : [Quarto](https://quarto.org/docs/get-started/) (inclut Pandoc et Typst).
```
quarto render # tous les supports
quarto render modules/03-pwm-adc.qmd # un seul support
```
Les PDF sont créés dans `_output/modules/`.
Pour obtenir aussi du Word ou de l'ODT, ajouter `docx:` ou `odt:` sous `format:` dans `_quarto.yml`.
## Modifier ou ajouter un schéma
Les images de `schemas/svg/` sont versionnées : il n'est nécessaire de les régénérer que si l'on modifie un schéma.
```
python -m venv .venv
.venv\Scripts\python -m pip install -r schemas/requirements.txt
.venv\Scripts\python schemas\generer.py # ou : generer.py m03
```
Un schéma est une fonction Python déclarée dans `schemas/mXX_*.py` (liste `FIGURES`), puis appelée dans le `.qmd` :
```markdown
![Légende de la figure](../schemas/svg/m03-servo-50hz.svg){#fig-m03-servo-50hz}
```
## Historique
Les supports étaient à l'origine au format `.odt` ; ils restent récupérables via le tag `avant-quarto`.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
project:
type: default
output-dir: _output
render:
- modules/*.qmd
lang: fr
subtitle: "Formation ESP32-S3 avec Arduino Framework"
crossref:
fig-title: Figure
fig-prefix: figure
format:
typst:
papersize: a4
margin:
x: 2cm
y: 2.2cm
mainfont: "Segoe UI"
fontsize: 10pt
codefont: "Consolas"
toc: false
highlight-style: modele/code.theme
include-in-header: modele/entete.typ
+217
View File
@@ -0,0 +1,217 @@
{
"custom-styles": {
"INI Files": {
"Section": {
"selected-text-color": "#6f42c1",
"text-color": "#6f42c1"
}
},
"Python": {
"Builtin Function": {
"selected-text-color": "#6f42c1",
"text-color": "#6f42c1"
},
"Import": {
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"Special Variable": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
}
},
"Rust": {
"Attribute": {
"selected-text-color": "#24292e",
"text-color": "#24292e"
},
"Macro": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
},
"Self": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
},
"Trait": {
"selected-text-color": "#24292e",
"text-color": "#24292e"
}
},
"XML": {
"Element": {
"selected-text-color": "#22863a",
"text-color": "#22863a"
}
}
},
"editor-colors": {
"BackgroundColor": "#ffffff",
"BracketMatching": "#bef5cb",
"CodeFolding": "#f6f8fa",
"CurrentLine": "#f6f8fa",
"CurrentLineNumber": "#24292e",
"IconBorder": "#ffffff",
"IndentationLine": "#d7dbe0",
"LineNumbers": "#c7c2bc",
"MarkBookmark": "#8be9fd",
"MarkBreakpointActive": "#ff5555",
"MarkBreakpointDisabled": "#bd93f9",
"MarkBreakpointReached": "#f1fa8c",
"MarkError": "#b31d28",
"MarkExecution": "#44475a",
"MarkWarning": "#e36209",
"ModifiedLines": "#d73a49",
"ReplaceHighlight": "#50fa7b",
"SavedLines": "#28a745",
"SearchHighlight": "#ffea7f",
"Separator": "#e1e4e8",
"SpellChecking": "#ff5555",
"TabMarker": "#d1d5da",
"TemplateBackground": "#23241e",
"TemplateFocusedPlaceholder": "#22231d",
"TemplatePlaceholder": "#22231d",
"TemplateReadOnlyPlaceholder": "#262721",
"TextSelection": "#dee6fc",
"WordWrapMarker": "#e1e4e8"
},
"metadata": {
"copyright": [
"SPDX-FileCopyrightText: 2020 GitHub Inc.",
"SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>"
],
"license": "SPDX-License-Identifier: MIT",
"name": "GitHub Light",
"revision": 2
},
"text-styles": {
"Alert": {
"bold": true,
"selected-text-color": "#ff5555",
"text-color": "#ff5555"
},
"Annotation": {
"selected-text-color": "#6a737d",
"text-color": "#6a737d"
},
"Attribute": {
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"BaseN": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
},
"BuiltIn": {
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"Char": {
"selected-text-color": "#032f62",
"text-color": "#032f62"
},
"Comment": {
"selected-text-color": "#6a737d",
"text-color": "#6a737d"
},
"CommentVar": {
"selected-text-color": "#6a737d",
"text-color": "#6a737d"
},
"Constant": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
},
"ControlFlow": {
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"DataType": {
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"DecVal": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
},
"Documentation": {
"selected-text-color": "#6a737d",
"text-color": "#6a737d"
},
"Error": {
"selected-text-color": "#ff5555",
"text-color": "#ff5555",
"underline": true
},
"Extension": {
"bold": true,
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"Float": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
},
"Function": {
"selected-text-color": "#6f42c1",
"text-color": "#6f42c1"
},
"Import": {
"selected-text-color": "#032f62",
"text-color": "#032f62"
},
"Information": {
"selected-text-color": "#6a737d",
"text-color": "#6a737d"
},
"Keyword": {
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"Normal": {
"selected-text-color": "#24292e",
"text-color": "#24292e"
},
"Operator": {
"selected-text-color": "#24292e",
"text-color": "#24292e"
},
"Others": {
"selected-text-color": "#6f42c1",
"text-color": "#6f42c1"
},
"Preprocessor": {
"selected-text-color": "#d73a49",
"text-color": "#d73a49"
},
"RegionMarker": {
"selected-text-color": "#6a737d",
"text-color": "#6a737d"
},
"SpecialChar": {
"selected-text-color": "#005cc5",
"text-color": "#005cc5"
},
"SpecialString": {
"selected-text-color": "#032f62",
"text-color": "#032f62"
},
"String": {
"selected-text-color": "#032f62",
"text-color": "#032f62"
},
"Variable": {
"selected-text-color": "#e36209",
"text-color": "#e36209"
},
"VerbatimString": {
"selected-text-color": "#032f62",
"text-color": "#032f62"
},
"Warning": {
"selected-text-color": "#ff5555",
"text-color": "#ff5555"
}
},
"background-color": "#f6f8fa"
}
+47
View File
@@ -0,0 +1,47 @@
// Note : Quarto décale les titres d'un niveau (## -> niveau 1, ### -> niveau 2).
// Gabarit commun des supports : couleurs, titres, en-tête et pied de page.
#let bleu-formation = rgb("#1a5fb4")
#let gris-doux = rgb("#6b7280")
#set page(
header: context {
// Grille plutôt que #h(1fr), que le préambule de Quarto neutralise dans l'en-tête
if counter(page).get().first() > 1 {
set text(size: 8pt, fill: gris-doux)
grid(columns: (1fr, auto), [Formation ESP32-S3 avec Arduino Framework], document.title)
v(-4pt)
line(length: 100%, stroke: 0.4pt + gris-doux)
}
},
footer: context {
set text(size: 8pt, fill: gris-doux)
align(right, counter(page).display("1 / 1", both: true))
},
)
#show heading.where(level: 1): it => block(above: 1.6em, below: 0.8em, width: 100%)[
#set text(size: 14pt, weight: "semibold", fill: bleu-formation)
#it.body
#v(-6pt)
#line(length: 100%, stroke: 0.6pt + bleu-formation.lighten(60%))
]
#show heading.where(level: 2): set text(size: 11.5pt, weight: "semibold", fill: bleu-formation)
#show heading.where(level: 2): set block(above: 1.3em, below: 0.6em)
// Code : le fond et les couleurs viennent du thème modele/code.theme
#show raw: set text(size: 8.5pt)
// Tableaux : en-tête bleu
#set table(
stroke: 0.5pt + rgb("#d0d7de"),
fill: (_, y) => if y == 0 { bleu-formation } else if calc.even(y) { rgb("#f6f8fa") },
)
#show table.cell.where(y: 0): set text(fill: white, weight: "bold")
#show figure.caption: set text(size: 8.5pt, style: "italic", fill: gris-doux)
// 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%)
+119
View File
@@ -0,0 +1,119 @@
---
title: "Module 0 — Prérequis & Installation"
---
## Objectifs du module
- Comprendre le rôle et les avantages de l'Arduino IDE pour programmer un ESP32-S3.
- Installer et configurer l'environnement de développement.
- Identifier et installer les pilotes nécessaires à la reconnaissance de la carte.
- Flasher un premier programme (Blink) et valider la chaîne complète (code → compilation → upload → exécution).
## 1. Matériel utilisé dans cette formation
Toute la formation s'appuie sur une carte de développement ESP32-S3-WROOM-1 (DevKitC-1 ou équivalent). Points clés à connaître dès maintenant :
- Microcontrôleur Xtensa LX7 double cœur, cadencé jusqu'à 240 MHz.
- Connexion USB native (USB OTG) : pas besoin de convertisseur USB-série externe sur la plupart des DevKits S3.
- Une LED RGB adressable (NeoPixel / WS2812) intégrée sur la carte — nous l'utiliserons au Module 2.
- Wi-Fi 2,4 GHz + Bluetooth Low Energy (BLE) — attention : pas de Bluetooth Classic sur l'ESP32-S3.
- Alimentation 3,3 V logique (contrairement à l'Arduino Uno qui est en 5 V) : ne jamais appliquer plus de 3,3 V sur une broche GPIO.
::: {.callout-note title="À vérifier avant de commencer"}
Le modèle exact de votre carte peut varier (nombre de broches, position de la LED NeoPixel, présence de PSRAM). Repérez sur votre carte la sérigraphie indiquant le numéro de chaque GPIO : elle servira de référence tout au long de la formation.
:::
## 2. Installation de l'Arduino IDE
### 2.1 Téléchargement
Rendez-vous sur le site officiel arduino.cc/en/software et téléchargez la version 2.x de l'Arduino IDE (interface plus moderne, gestion de projets multiples, terminal série amélioré) pour votre système d'exploitation (Windows, macOS, Linux).
### 2.2 Installation des pilotes USB
Selon la puce USB-série présente sur votre carte (souvent inutile si la carte utilise l'USB natif du S3), il peut être nécessaire d'installer un pilote :
- CP210x (Silicon Labs) — pour certains DevKits.
- CH340/CH343 — pour d'autres clones.
Sur Windows, le Gestionnaire de périphériques doit afficher un port COM une fois la carte branchée. Sur macOS/Linux, la carte apparaît généralement automatiquement (/dev/tty.usbserial-\* ou /dev/ttyUSB\*/ttyACM\*).
## 3. Ajout du support ESP32 dans l'Arduino IDE
Par défaut, l'IDE ne connaît que les cartes Arduino officielles. Il faut ajouter le paquet de définitions Espressif :
### Étapes
- Ouvrir Fichier \> Préférences (ou Arduino IDE \> Réglages sur macOS).
- Dans « URL de gestionnaire de cartes supplémentaires », ajouter :
```text
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
```
- Ouvrir Outils \> Type de carte \> Gestionnaire de cartes.
- Rechercher « esp32 » et installer le paquet « esp32 by Espressif Systems » (dernière version stable).
- Redémarrer l'IDE.
## 4. Sélection et configuration de la carte
Une fois le paquet installé :
- Outils \> Type de carte \> ESP32 Arduino \> choisir « ESP32S3 Dev Module ».
- Outils \> Port : sélectionner le port série apparu à la connexion de la carte.
- Outils \> USB CDC On Boot : Enabled (recommandé pour voir les messages Serial dès le démarrage sur l'ESP32-S3).
- Outils \> Upload Speed : 921600 (par défaut, réduire en cas d'échec d'upload).
::: {.callout-tip title="Astuce mode téléchargement"}
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.
:::
![Forcer le mode téléchargement : séquence des boutons BOOT et RESET](../schemas/svg/m00-boot-reset.svg){#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.
![La chaîne de développement : du croquis à l'exécution sur la carte, puis retour par le moniteur série](../schemas/svg/m00-chaine-developpement.svg){#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.
#define LED_PIN 2
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
Serial.println("Demarrage du programme Blink");
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
Serial.println("Clignotement...");
}
```
Compilez avec le bouton ✓ (Vérifier), puis téléversez avec la flèche → (Téléverser). Ouvrez le Moniteur Série (Outils \> Moniteur Série, vitesse 115200 bauds) pour voir les messages s'afficher.
## 6. Checklist de fin de module
- [ ] Arduino IDE installé et à jour.
- [ ] Pilotes USB installés si nécessaire, port COM/tty visible.
- [ ] Paquet ESP32 (Espressif) installé via le gestionnaire de cartes.
- [ ] Carte « ESP32S3 Dev Module » sélectionnée avec le bon port.
- [ ] Programme Blink compilé, téléversé et fonctionnel.
- [ ] Moniteur série fonctionnel (messages visibles à 115200 bauds).
## Exercice pratique
Modifiez le programme Blink pour :
- Faire clignoter la LED avec un rythme irrégulier (ex : court, court, long — façon morse).
- Afficher dans le moniteur série un compteur du nombre de clignotements depuis le démarrage.
## Pour aller plus loin (facultatif)
Consultez la documentation officielle Espressif sur le cœur Arduino pour ESP32 : elle liste l'ensemble des cartes supportées et des options spécifiques (partitions, PSRAM, etc.) que nous détaillerons au Module 1.
+108
View File
@@ -0,0 +1,108 @@
---
title: "Module 1 — Découverte du microcontrôleur ESP32-S3"
---
## Objectifs du module
- Comprendre ce qu'est un microcontrôleur et en quoi il diffère d'un ordinateur classique.
- Connaître l'architecture interne de l'ESP32-S3 (cœurs, mémoire, périphériques).
- Identifier les différences clés avec un Arduino Uno/Nano classique.
- Repérer les broches spéciales à éviter (strapping pins) et lire un brochage (pinout).
## 1. Qu'est-ce qu'un microcontrôleur ?
Un microcontrôleur est un circuit intégré unique regroupant un processeur, de la mémoire vive (RAM), de la mémoire de programme (Flash) et des périphériques d'entrée/sortie (GPIO, ADC, timers, bus de communication). Contrairement à un ordinateur, il n'exécute généralement qu'un seul programme en boucle, sans système d'exploitation complexe, ce qui garantit un comportement déterministe et une consommation réduite.
L'ESP32-S3 va plus loin qu'un microcontrôleur classique (type ATmega328 de l'Arduino Uno) en intégrant nativement du Wi-Fi et du Bluetooth Low Energy, ce qui en fait un choix privilégié pour les projets connectés (IoT).
## 2. Architecture interne de l'ESP32-S3
![Architecture simplifiée de l'ESP32-S3 et de son module](../schemas/svg/m01-architecture.svg){#fig-m01-architecture}
### 2.1 Cœurs de calcul
- Double cœur Xtensa LX7 cadencé jusqu'à 240 MHz.
- Un cœur peut être dédié à des tâches critiques (ex : communication réseau) pendant que l'autre exécute la logique applicative — via FreeRTOS, présent nativement sous le framework Arduino.
### 2.2 Mémoire
- SRAM interne : environ 512 Ko, partagée entre données et instructions.
- Mémoire Flash externe (souvent 8 ou 16 Mo selon la carte) : stocke le programme (sketch) et éventuellement un système de fichiers (SPIFFS/LittleFS).
- PSRAM (RAM externe, optionnelle selon le modèle de carte) : utile pour le traitement d'images ou de gros buffers.
### 2.3 Connectivité intégrée
- Wi-Fi 802.11 b/g/n (2,4 GHz).
- Bluetooth 5 LE (Low Energy uniquement — pas de Bluetooth Classic/SPP sur le S3, contrairement à l'ESP32 original).
### 2.4 Interface USB native
L'ESP32-S3 dispose d'un contrôleur USB OTG natif, permettant la programmation et la communication série directement via USB, sans puce USB-série intermédiaire (selon la carte). Il peut aussi émuler un périphérique USB (clavier, souris, disque) — nous y reviendrons dans un module avancé.
## 3. ESP32-S3 vs Arduino Uno : les différences essentielles
| Caractéristique | Arduino Uno (ATmega328) | ESP32-S3 |
|-------------------|---------------------------|-------------------------------------|
| Cœurs / fréquence | 1 cœur, 16 MHz | 2 cœurs, jusqu'à 240 MHz |
| Tension logique | 5 V | 3,3 V |
| RAM | 2 Ko | \~512 Ko |
| Flash programme | 32 Ko | 8-16 Mo (typique) |
| Wi-Fi / Bluetooth | Non (externe requis) | Intégrés (Wi-Fi + BLE) |
| Broches ADC | 10 bits, 5 entrées | 12 bits, jusqu'à 20 entrées |
| PWM | Limité (Timer matériel) | LEDC — jusqu'à 8 canaux configurables |
![Arduino Uno et ESP32-S3 : ordres de grandeur (échelle logarithmique)](../schemas/svg/m01-uno-vs-s3.svg){#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.
:::
![Abaisser un signal 5 V à 3,3 V avec un diviseur de tension](../schemas/svg/m01-diviseur-tension.svg){#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 :
- Les broches d'alimentation : 5V (VIN/USB), 3V3, GND.
- Les broches GPIO numérotées (GPIO0 à GPIO21 selon les cartes, tous ne sont pas exposés).
- Les broches ADC (généralement GPIO1 à GPIO10 sur l'unité ADC1).
- La broche pilotant la LED NeoPixel intégrée (souvent GPIO48, à vérifier sur la sérigraphie ou le schéma de votre carte spécifique).
### Broches à éviter (strapping pins)
Certaines broches ont un rôle spécial au démarrage (sélection du mode de boot) et peuvent perturber le fonctionnement de la carte si elles sont utilisées comme sorties classiques ou reliées à des composants qui tirent le niveau logique au démarrage :
- GPIO0 : bouton BOOT — à éviter comme sortie générale.
- GPIO3, GPIO45, GPIO46 : broches de strapping — à utiliser avec prudence, éviter en sortie forte au démarrage.
- GPIO19/GPIO20 : utilisées par l'USB natif sur de nombreuses cartes S3 — à éviter si vous utilisez la connexion USB.
::: {.callout-tip title="Bonne pratique"}
Gardez toujours sous la main le schéma (pinout diagram) exact de votre référence de carte. Deux cartes vendues comme « ESP32-S3 DevKit » par des fabricants différents peuvent avoir un brochage légèrement différent.
:::
## 5. Exercice pratique : inventaire de votre carte
Sans câblage, à l'aide de la documentation de votre carte :
- Dessinez ou annotez un schéma de votre carte en identifiant : alimentation, GND, GPIO utilisables, broches à éviter, broche de la LED NeoPixel.
- Écrivez un programme qui affiche dans le moniteur série, au démarrage, la fréquence CPU (`getCpuFrequencyMhz()`), la taille de la mémoire Flash (`ESP.getFlashChipSize()`) et la mémoire heap libre (`ESP.getFreeHeap()`).
```cpp
void setup() {
Serial.begin(115200);
delay(1000);
Serial.printf("Frequence CPU : %d MHz\n", getCpuFrequencyMhz());
Serial.printf("Taille Flash : %d octets\n", ESP.getFlashChipSize());
Serial.printf("Heap libre : %d octets\n", ESP.getFreeHeap());
}
void loop() {
}
```
## Checklist de fin de module
- [ ] Je sais expliquer la différence entre RAM et Flash sur ce microcontrôleur.
- [ ] Je connais les broches à éviter (strapping pins) sur ma carte.
- [ ] J'ai identifié la broche pilotant la LED NeoPixel intégrée.
- [ ] J'ai exécuté le programme d'inventaire et compris les valeurs affichées.
+183
View File
@@ -0,0 +1,183 @@
---
title: "Module 2 — Entrées/Sorties numériques & LED RGB NeoPixel"
---
## Objectifs du module
- Maîtriser `digitalWrite()` et `digitalRead()` sur l'ESP32-S3.
- Comprendre les résistances de tirage (pull-up / pull-down) et l'anti-rebond logiciel.
- Réaliser un chenillard de LED et une lecture de bouton-poussoir.
- Piloter la LED RGB NeoPixel intégrée à la carte (protocole WS2812, bibliothèque Adafruit_NeoPixel).
## 1. Rappels : sorties numériques
Une broche GPIO configurée en sortie (OUTPUT) ne peut prendre que deux états électriques : HIGH (3,3 V) ou LOW (0 V). C'est la base de tout pilotage de LED, relais, transistor de puissance, etc.
```cpp
#define LED_PIN 4
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(300);
digitalWrite(LED_PIN, LOW);
delay(300);
}
```
## 2. Rappels : entrées numériques et résistances de tirage
Une entrée numérique non connectée (« flottante ») peut lire un état aléatoire à cause du bruit électrique. Il faut donc fixer son état par défaut avec une résistance de tirage :
- `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.
![Bouton sur une entrée en `INPUT_PULLUP` : la résistance de tirage interne fixe l'état au repos](../schemas/svg/m02-pull-up.svg){#fig-m02-pull-up width=70%}
```cpp
#define BUTTON_PIN 5
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
int etat = digitalRead(BUTTON_PIN);
if (etat == LOW) {
Serial.println("Bouton presse");
}
delay(50);
}
```
::: {.callout-note title="Anti-rebond (debounce)"}
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).
:::
![Rebonds d'un bouton mécanique : l'anti-rebond ne retient qu'un seul appui](../schemas/svg/m02-rebonds.svg){#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 :
```cpp
const int leds[] = {4, 5, 6, 7};
const int nbLeds = 4;
void setup() {
for (int i = 0; i < nbLeds; i++) {
pinMode(leds[i], OUTPUT);
}
}
void loop() {
for (int i = 0; i < nbLeds; i++) {
digitalWrite(leds[i], HIGH);
delay(150);
digitalWrite(leds[i], LOW);
}
}
```
![Câblage du chenillard : une résistance de 220 Ω par LED, masse commune](../schemas/svg/m02-chenillard.svg){#fig-m02-chenillard width=75%}
## 4. La LED RGB NeoPixel intégrée
Contrairement à une LED classique qui ne prend que deux états (allumé/éteint) ou une intensité en PWM, la LED intégrée sur la plupart des cartes ESP32-S3 DevKit est une LED adressable de type WS2812 (aussi appelée NeoPixel). Elle intègre son propre contrôleur et se pilote avec un protocole série à un seul fil, où chaque LED reçoit une trame de 24 bits (8 bits rouge, 8 bits vert, 8 bits bleu).
### 4.1 Pourquoi ce n'est pas un simple digitalWrite
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.
![Protocole WS2812 : trame de 24 bits et codage temporel de chaque bit](../schemas/svg/m02-ws2812.svg){#fig-m02-ws2812}
### 4.2 Installation de la bibliothèque
- Dans l'IDE : Croquis \> Inclure une bibliothèque \> Gérer les bibliothèques.
- Rechercher « Adafruit NeoPixel » et installer la dernière version (ainsi que sa dépendance Adafruit BusIO si demandée).
::: {.callout-note title="Identifier la broche de la LED"}
Sur la plupart des cartes ESP32-S3-DevKitC-1, la LED NeoPixel est reliée au GPIO48 et nécessite d'alimenter une broche annexe (souvent GPIO47 sur certaines révisions) pour l'activer. Vérifiez impérativement le schéma de votre carte : certains modèles utilisent GPIO38 ou un autre GPIO. Le code ci-dessous utilise une constante `NEOPIXEL_PIN` à adapter.
:::
### 4.3 Premier programme : allumer la LED en rouge
```cpp
#include <Adafruit_NeoPixel.h>
#define NEOPIXEL_PIN 48 // A adapter selon votre carte
#define NB_PIXELS 1
Adafruit_NeoPixel pixels(NB_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
void setup() {
pixels.begin();
pixels.setBrightness(50); // Luminosite 0-255, a limiter pour ne pas eblouir
pixels.setPixelColor(0, pixels.Color(255, 0, 0)); // Rouge
pixels.show();
}
void loop() {
}
```
### 4.4 Exercice : cycle de couleurs et respiration
Objectif : faire défiler plusieurs couleurs, puis réaliser un effet de « respiration » (variation progressive de la luminosité) sur une couleur fixe.
```cpp
#include <Adafruit_NeoPixel.h>
#define NEOPIXEL_PIN 48
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
void setup() {
pixel.begin();
pixel.setBrightness(80);
}
void loop() {
// Cycle de couleurs
pixel.setPixelColor(0, pixel.Color(255, 0, 0)); pixel.show(); delay(400);
pixel.setPixelColor(0, pixel.Color(0, 255, 0)); pixel.show(); delay(400);
pixel.setPixelColor(0, pixel.Color(0, 0, 255)); pixel.show(); delay(400);
// Effet respiration en bleu
for (int b = 0; b <= 255; b += 5) {
pixel.setBrightness(b);
pixel.setPixelColor(0, pixel.Color(0, 0, 255));
pixel.show();
delay(15);
}
for (int b = 255; b >= 0; b -= 5) {
pixel.setBrightness(b);
pixel.setPixelColor(0, pixel.Color(0, 0, 255));
pixel.show();
delay(15);
}
}
```
## 5. Exercice final : bouton + NeoPixel
Combinez les notions du module : à chaque appui sur un bouton-poussoir (avec `INPUT_PULLUP` et anti-rebond), la LED NeoPixel doit changer de couleur en suivant un cycle défini (rouge → vert → bleu → blanc → éteint → rouge...).
| Étape | Comportement attendu |
|---------|------------------------------------|
| Démarrage | LED éteinte |
| 1er appui | LED rouge |
| 2e appui | LED verte |
| 3e appui | LED bleue |
| 4e appui | LED blanche |
| 5e appui | Retour à éteint, le cycle recommence |
## Checklist de fin de module
- [ ] Je sais configurer et lire un bouton avec `INPUT_PULLUP`.
- [ ] Je comprends pourquoi un debounce est nécessaire.
- [ ] J'ai identifié la broche exacte de la LED NeoPixel sur ma carte.
- [ ] J'ai réalisé l'exercice combinant bouton et changement de couleur.
+162
View File
@@ -0,0 +1,162 @@
---
title: "Module 3 — Signaux analogiques : PWM & ADC"
---
## Objectifs du module
- Comprendre le principe du PWM (modulation de largeur d'impulsion) et son usage sur ESP32-S3 via l'API LEDC.
- Réaliser un variateur de luminosité et un contrôle de servomoteur.
- Comprendre le fonctionnement du convertisseur analogique-numérique (ADC) et ses limites.
- Lire un potentiomètre et convertir la valeur brute en unité physique.
## 1. Qu'est-ce que le PWM ?
Le PWM (Pulse Width Modulation) simule une sortie analogique en faisant varier rapidement le rapport cyclique (duty cycle) d'un signal carré : plus la portion « HIGH » est longue par rapport à la période, plus la valeur moyenne perçue (tension, luminosité, vitesse) est élevée. C'est la technique utilisée pour faire varier la luminosité d'une LED ou piloter un servomoteur.
![Le rapport cyclique fixe la tension moyenne perçue par la charge (LED, moteur)](../schemas/svg/m03-pwm-rapport-cyclique.svg){#fig-m03-pwm-rapport-cyclique}
## 2. Le PWM sur ESP32-S3 : l'API LEDC
Contrairement à un Arduino Uno où `analogWrite()` suffit, l'ESP32 utilise un contrôleur matériel dédié appelé LEDC (LED Control), plus flexible (fréquence et résolution configurables) mais nécessitant une configuration explicite. Depuis les versions récentes du core Arduino-ESP32, une API simplifiée `analogWrite()` est également disponible et gère LEDC en interne — nous verrons les deux approches.
### 2.1 Approche simplifiée : `analogWrite()`
```cpp
#define LED_PIN 4
void setup() {
// Rien de particulier a configurer avec l'API simplifiee
}
void loop() {
for (int duty = 0; duty <= 255; duty++) {
analogWrite(LED_PIN, duty); // 0 = eteint, 255 = pleine puissance
delay(10);
}
for (int duty = 255; duty >= 0; duty--) {
analogWrite(LED_PIN, duty);
delay(10);
}
}
```
### 2.2 Approche explicite : API LEDC (contrôle fin)
Utile lorsqu'on a besoin d'une fréquence ou d'une résolution précise (par exemple pour piloter un servomoteur, qui attend une fréquence de 50 Hz).
![Commande d'un servomoteur : la largeur de l'impulsion (1 à 2 ms) fixe l'angle](../schemas/svg/m03-servo-50hz.svg){#fig-m03-servo-50hz}
```cpp
#define LED_PIN 4
#define PWM_FREQ 5000 // 5 kHz
#define PWM_RES 8 // resolution 8 bits (0-255)
void setup() {
ledcAttach(LED_PIN, PWM_FREQ, PWM_RES);
}
void loop() {
for (int duty = 0; duty <= 255; duty++) {
ledcWrite(LED_PIN, duty);
delay(10);
}
}
```
::: {.callout-note title="Version du core Arduino-ESP32"}
L'API LEDC a changé entre les versions 2.x et 3.x du core Arduino-ESP32 (l'ancienne API utilisait des numéros de canal explicites via ledcSetup/ledcAttachPin). Vérifiez la version installée dans le gestionnaire de cartes si le code ci-dessus ne compile pas, et adaptez-vous à la documentation correspondante.
:::
## 3. Exercice : variateur de luminosité avec potentiomètre
Nous allons combiner PWM et lecture analogique : la position d'un potentiomètre définit la luminosité d'une LED.
![Câblage de l'exercice : potentiomètre sur GPIO1 (ADC1), LED sur GPIO4 (PWM)](../schemas/svg/m03-montage-variateur.svg){#fig-m03-montage-variateur}
## 4. Le convertisseur analogique-numérique (ADC)
L'ADC convertit une tension continue (0 à 3,3 V) en une valeur numérique. L'ESP32-S3 dispose d'un ADC 12 bits, soit une résolution de 4096 valeurs possibles (0 à 4095).
![Conversion analogique-numérique puis mise à l'échelle avec map()](../schemas/svg/m03-adc-quantification.svg){#fig-m03-adc-quantification}
| Caractéristique | Valeur |
|-----------------------------------|----------------------------------------|
| Résolution native | 12 bits (0 à 4095) |
| Plage de tension | 0 à 3,3 V (selon atténuation configurée) |
| Unité ADC utilisable en Wi-Fi actif | ADC1 uniquement (ADC2 partagé avec le Wi-Fi) |
| Fonction de lecture | `analogRead(pin)` |
::: {.callout-warning title="Limitation importante : ADC2 et Wi-Fi"}
L'ADC2 de l'ESP32 partage des ressources internes avec le module Wi-Fi. Si le Wi-Fi est actif (voir Module 7), les lectures sur les broches ADC2 deviennent peu fiables voire impossibles. Pour tout projet connecté utilisant l'analogique, privilégiez les broches de l'ADC1 (généralement GPIO1 à GPIO10 sur l'ESP32-S3).
:::
### 4.1 Lire un potentiomètre
```cpp
#define POT_PIN 1 // Broche ADC1
void setup() {
Serial.begin(115200);
}
void loop() {
int valeurBrute = analogRead(POT_PIN); // 0 a 4095
float tension = valeurBrute * (3.3 / 4095.0); // conversion en volts
Serial.printf("Brut: %d Tension: %.2f V\n", valeurBrute, tension);
delay(200);
}
```
### 4.2 Exercice combiné : variateur de LED par potentiomètre
Utilisez la valeur lue sur le potentiomètre (0-4095) pour piloter la luminosité d'une LED en PWM (0-255) en convertissant l'échelle avec la fonction `map()`.
```cpp
#define POT_PIN 1
#define LED_PIN 4
void setup() {
ledcAttach(LED_PIN, 5000, 8);
Serial.begin(115200);
}
void loop() {
int brut = analogRead(POT_PIN);
int duty = map(brut, 0, 4095, 0, 255);
ledcWrite(LED_PIN, duty);
Serial.printf("Potentiometre: %d -> PWM: %d\n", brut, duty);
delay(50);
}
```
## 5. Aller plus loin : appliquer le PWM à la LED NeoPixel
Pour rappel (Module 2), la LED NeoPixel ne se pilote pas avec analogWrite ou ledcWrite : elle utilise un protocole numérique dédié (WS2812) géré par la bibliothèque Adafruit_NeoPixel, dont la fonction `setBrightness()` joue un rôle équivalent à celui du PWM pour l'intensité globale. Reliez les deux notions : adaptez l'exercice du variateur pour que ce soit cette fois la luminosité de la LED NeoPixel qui varie selon le potentiomètre.
```cpp
#include <Adafruit_NeoPixel.h>
#define POT_PIN 1
#define NEOPIXEL_PIN 48
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
void setup() {
pixel.begin();
pixel.setPixelColor(0, pixel.Color(255, 255, 255));
}
void loop() {
int brut = analogRead(POT_PIN);
int luminosite = map(brut, 0, 4095, 0, 255);
pixel.setBrightness(luminosite);
pixel.show();
delay(50);
}
```
## Checklist de fin de module
- [ ] Je sais expliquer le principe du duty cycle en PWM.
- [ ] Je sais configurer une sortie PWM avec analogWrite ou l'API LEDC.
- [ ] Je connais la résolution et les limites de l'ADC de l'ESP32-S3 (notamment ADC2 + Wi-Fi).
- [ ] J'ai réalisé le variateur de luminosité piloté par potentiomètre, sur LED classique et sur NeoPixel.
+150
View File
@@ -0,0 +1,150 @@
---
title: "Module 4 — Communication série & débogage"
---
## Objectifs du module
- Comprendre le fonctionnement du bus UART et son rôle central dans le débogage.
- Distinguer les différents ports série disponibles sur l'ESP32-S3.
- Utiliser efficacement le moniteur série et les fonctions d'affichage formaté.
- Construire une petite console de commandes interactive.
## 1. Rappel : qu'est-ce que l'UART ?
L'UART (Universal Asynchronous Receiver-Transmitter) est un protocole de communication série point à point utilisant deux fils : TX (transmission) et RX (réception), sans horloge partagée — les deux extrémités doivent donc être configurées à la même vitesse (baud rate) pour interpréter correctement les bits reçus.
![Trame UART 8N1 : envoi du caractère « A » (0x41)](../schemas/svg/m04-trame-uart.svg){#fig-m04-trame-uart}
Sur l'ESP32-S3, la liaison série vers l'ordinateur (moniteur série de l'IDE) transite le plus souvent directement par le port USB natif du microcontrôleur (USB CDC), et non par un UART matériel classique relié à une puce USB-série externe — d'où l'importance de l'option « USB CDC On Boot » vue au Module 0.
## 2. Les ports série disponibles
L'ESP32-S3 dispose de plusieurs contrôleurs UART matériels, utilisables simultanément pour des usages différents (ex : un port pour le debug, un autre pour dialoguer avec un module GPS ou un lecteur RFID).
| Objet | Usage typique |
|---------|----------------------------------------|
| Serial | Console USB (moniteur série / debug), via USB CDC sur l'ESP32-S3 |
| Serial1 | UART matériel n°1, broches à réassigner explicitement |
| Serial2 | UART matériel n°2, broches à réassigner explicitement |
::: {.callout-note title="Réassignation des broches UART"}
Sur l'ESP32-S3, les broches des UART matériels ne sont pas fixes : on les définit avec Serial1.begin(baud, config, rxPin, txPin). Vérifiez que les broches choisies ne font pas partie des broches à éviter identifiées au Module 1.
:::
```cpp
// Exemple : Serial2 sur les broches GPIO17 (RX) et GPIO18 (TX)
#define RX2 17
#define TX2 18
void setup() {
Serial.begin(115200); // console USB
Serial2.begin(9600, SERIAL_8N1, RX2, TX2); // module externe
}
void loop() {
if (Serial2.available()) {
String recu = Serial2.readStringUntil('\n');
Serial.print("Recu du module: ");
Serial.println(recu);
}
}
```
![Deux liaisons série simultanées : Serial vers l'ordinateur, Serial2 vers un module (TX et RX croisés)](../schemas/svg/m04-cablage-serie.svg){#fig-m04-cablage-serie}
## 3. Bien utiliser le moniteur série
### 3.1 Fonctions d'affichage
| Fonction | Rôle |
|-----------------------|----------------------------------------|
| `Serial.print(x)` | Affiche x sans retour à la ligne |
| `Serial.println(x)` | Affiche x avec un retour à la ligne |
| Serial.printf(fmt, ...) | Affichage formaté façon C (%d, %s, %f, %.2f...) |
```cpp
int temperature = 24;
float tension = 3.298;
Serial.printf("Temperature: %d C, Tension: %.2f V\n", temperature, tension);
```
::: {.callout-tip title="Bonne pratique de debug"}
Préférez `Serial.printf()` à la concaténation de multiples `Serial.print()` : le code est plus lisible et plus facile à maintenir, surtout lorsqu'on affiche plusieurs variables à la fois.
:::
## 4. Lire des données envoyées depuis le moniteur série
Le moniteur série permet aussi d'envoyer du texte vers la carte, ce qui est très utile pour créer des interfaces de test simples (changer un paramètre sans reflasher le programme).
```cpp
void setup() {
Serial.begin(115200);
Serial.println("Tapez une commande puis Entree");
}
void loop() {
if (Serial.available()) {
String commande = Serial.readStringUntil('\n');
commande.trim(); // supprime espaces/retours de fin
Serial.print("Commande recue: ");
Serial.println(commande);
}
}
```
## 5. Exercice pratique : mini console de commandes
En vous appuyant sur la LED NeoPixel du Module 2, développez une petite console pilotée par le moniteur série :
- La commande « rouge » allume la LED en rouge.
- La commande « vert » l'allume en vert.
- La commande « bleu » l'allume en bleu.
- La commande « off » l'éteint.
- Toute commande inconnue affiche un message d'erreur explicite.
```cpp
#include <Adafruit_NeoPixel.h>
#define NEOPIXEL_PIN 48
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
void setup() {
Serial.begin(115200);
pixel.begin();
Serial.println("Commandes disponibles: rouge, vert, bleu, off");
}
void loop() {
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
cmd.toLowerCase();
if (cmd == "rouge") {
pixel.setPixelColor(0, pixel.Color(255, 0, 0));
} else if (cmd == "vert") {
pixel.setPixelColor(0, pixel.Color(0, 255, 0));
} else if (cmd == "bleu") {
pixel.setPixelColor(0, pixel.Color(0, 0, 255));
} else if (cmd == "off") {
pixel.setPixelColor(0, pixel.Color(0, 0, 0));
} else {
Serial.println("Commande inconnue.");
return;
}
pixel.show();
Serial.println("OK");
}
}
```
## 6. Pour aller plus loin (facultatif)
Ajoutez une commande « couleur R G B » qui accepte trois nombres séparés par des espaces (ex : couleur 128 0 255) en utilisant les fonctions `indexOf()` et `substring()` ou la fonction `sscanf()` pour extraire les valeurs de la chaîne reçue.
## Checklist de fin de module
- [ ] Je comprends la différence entre Serial, Serial1 et Serial2.
- [ ] Je sais réassigner les broches RX/TX d'un UART matériel.
- [ ] Je maîtrise `Serial.printf()` pour un affichage de debug formaté.
- [ ] J'ai réalisé la mini console de commandes pilotant la LED NeoPixel.
+183
View File
@@ -0,0 +1,183 @@
---
title: "Module 5 — Bus I2C et SPI"
---
## Objectifs du module
- Comprendre les principes des bus I2C et SPI et leurs différences.
- Câbler et adresser un périphérique I2C (scanner de bus, résolution de conflits d'adresse).
- Afficher du texte et des formes sur un écran OLED SSD1306 (I2C).
- Lire les données d'un capteur de température/humidité BME280 (I2C).
## 1. Pourquoi un bus de communication ?
Les broches GPIO génériques permettent de dialoguer avec un seul composant à la fois, en définissant vous-même le protocole. Pour standardiser les échanges avec des composants variés (écrans, capteurs, mémoires), l'industrie utilise des bus normalisés : I2C et SPI sont les deux plus répandus dans l'écosystème Arduino/ESP32.
## 2. Le bus I2C
### 2.1 Principe
I2C (Inter-Integrated Circuit) utilise seulement deux fils partagés par tous les périphériques du bus :
- SDA (Serial Data) : ligne de données.
- SCL (Serial Clock) : ligne d'horloge, générée par le maître (l'ESP32-S3 dans notre cas).
Chaque périphérique possède une adresse unique (7 bits, soit 128 adresses possibles) qui permet au maître de s'adresser spécifiquement à lui sur le bus partagé.
![Bus I2C : deux fils partagés, un maître et plusieurs esclaves adressés](../schemas/svg/m05-bus-i2c.svg){#fig-m05-bus-i2c}
### 2.2 Broches I2C par défaut sur ESP32-S3
Le framework Arduino-ESP32 définit par défaut SDA sur GPIO8 et SCL sur GPIO9 (à vérifier selon votre carte), mais ces broches peuvent être réassignées via Wire.begin(sda, scl).
```cpp
#include <Wire.h>
void setup() {
Wire.begin(8, 9); // SDA, SCL — a adapter selon votre carte
}
```
## 3. Scanner de bus I2C
![Trame I2C : écriture d'un octet vers l'adresse 0x3C (ce que teste le scanner : l'esclave répond-il par un ACK ?)](../schemas/svg/m05-trame-i2c.svg){#fig-m05-trame-i2c}
Avant de câbler un module, il est très utile de vérifier qu'il est bien détecté et de connaître son adresse exacte :
```cpp
#include <Wire.h>
void setup() {
Wire.begin(8, 9);
Serial.begin(115200);
delay(500);
Serial.println("Scan du bus I2C...");
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
byte erreur = Wire.endTransmission();
if (erreur == 0) {
Serial.printf("Peripherique trouve a l'adresse 0x%02X\n", addr);
}
}
Serial.println("Scan termine.");
}
void loop() {
}
```
::: {.callout-note title="Conflit d'adresse"}
Si deux modules identiques (deux écrans OLED par exemple) doivent cohabiter sur le même bus, ils doivent avoir des adresses différentes. Certains modules disposent de pastilles de soudure (solder jumpers) permettant de changer leur adresse d'usine.
:::
## 4. Exercice : afficheur OLED SSD1306
### 4.1 Installation des bibliothèques
- Adafruit GFX Library (moteur graphique générique).
- Adafruit SSD1306 (pilote spécifique à l'écran).
### 4.2 Câblage
| Broche écran | Broche ESP32-S3 |
|----------------|-----------------------------|
| VCC | 3V3 |
| GND | GND |
| SDA | GPIO8 (ou broche SDA choisie) |
| SCL | GPIO9 (ou broche SCL choisie) |
### 4.3 Premier affichage
```cpp
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C // Adresse trouvee avec le scanner I2C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
void setup() {
Wire.begin(8, 9);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("Ecran non detecte !");
while (true);
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Bonjour !");
display.display();
}
void loop() {
}
```
## 5. Exercice : capteur de température/humidité BME280
Le BME280 est un capteur environnemental (température, humidité, pression) communiquant en I2C, très utilisé dans les projets de station météo.
### 5.1 Installation de la bibliothèque
Installer « Adafruit BME280 Library » depuis le gestionnaire de bibliothèques.
### 5.2 Lecture des mesures
```cpp
#include <Wire.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
Wire.begin(8, 9);
if (!bme.begin(0x76)) { // ou 0x77 selon le module
Serial.println("BME280 non detecte !");
while (true);
}
}
void loop() {
float temperature = bme.readTemperature();
float humidite = bme.readHumidity();
float pression = bme.readPressure() / 100.0F; // hPa
Serial.printf("T: %.1f C H: %.1f %% P: %.1f hPa\n", temperature, humidite, pression);
delay(2000);
}
```
## 6. Exercice combiné : mini station météo
Combinez écran OLED et capteur BME280 pour afficher en temps réel la température et l'humidité, avec rafraîchissement toutes les 2 secondes. Ajoutez un indicateur visuel sur la LED NeoPixel : bleu si température \< 18°C, vert entre 18 et 25°C, rouge au-delà.
## 7. Le bus SPI : présentation
SPI (Serial Peripheral Interface) est plus rapide que l'I2C mais utilise davantage de fils : MOSI (données maître→esclave), MISO (esclave→maître), SCK (horloge) et une ligne CS (Chip Select) dédiée par périphérique. Il est privilégié pour les composants nécessitant un débit élevé (écrans TFT couleur, cartes SD, certains modules radio).
![Bus SPI : lignes de données et d'horloge partagées, une ligne CS par périphérique](../schemas/svg/m05-bus-spi.svg){#fig-m05-bus-spi}
| Critère | I2C | SPI |
|---------------|-----------------------|--------------------------------|
| Nombre de fils | 2 (partagés) | 4 (+1 CS par périphérique) |
| Vitesse typique | 100 kHz - 1 MHz | Plusieurs MHz à dizaines de MHz |
| Adressage | Adresse logicielle | Ligne CS physique dédiée |
| Usage typique | Capteurs, petits écrans | Écrans rapides, cartes SD, radio |
::: {.callout-note title="Approfondissement SPI"}
Le câblage et l'utilisation détaillée du SPI (ex : écran TFT, carte SD) seront pratiqués dans un module dédié aux projets avancés, en fonction du matériel disponible pour la formation.
:::
## Checklist de fin de module
- [ ] J'ai exécuté le scanner I2C et identifié les adresses de mes modules.
- [ ] J'ai affiché du texte sur l'écran OLED.
- [ ] J'ai lu température et humidité depuis le BME280.
- [ ] J'ai réalisé la mini station météo combinant écran, capteur et LED NeoPixel.
- [ ] Je sais expliquer la différence entre I2C et SPI.
+175
View File
@@ -0,0 +1,175 @@
---
title: "Module 6 — Interruptions matérielles & Timers"
---
## Objectifs du module
- Comprendre les limites de `delay()` et de la boucle `loop()` classique.
- Utiliser `millis()` pour gérer plusieurs tâches périodiques sans bloquer le programme.
- Mettre en œuvre une interruption matérielle avec `attachInterrupt()`.
- Comprendre le rôle de `IRAM_ATTR` et les bonnes pratiques dans une routine d'interruption (ISR).
## 1. Le problème de `delay()`
La fonction `delay()` bloque totalement l'exécution du programme pendant la durée spécifiée : aucune autre instruction (lecture de bouton, mise à jour d'écran, communication réseau) ne peut s'exécuter pendant ce temps. Pour un programme qui ne fait qu'une seule chose, ce n'est pas gênant ; mais dès qu'on veut gérer plusieurs comportements simultanés (ex : clignoter une LED tout en surveillant un bouton), `delay()` devient un frein majeur.
## 2. La technique du « blink sans delay » avec `millis()`
`millis()` renvoie le nombre de millisecondes écoulées depuis le démarrage du programme. En comparant cette valeur à un dernier instant mémorisé, on peut exécuter une action périodique sans jamais bloquer la boucle `loop()`.
```cpp
#define LED_PIN 4
unsigned long dernierChangement = 0;
const unsigned long intervalle = 500; // ms
bool etatLed = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
unsigned long maintenant = millis();
if (maintenant - dernierChangement >= intervalle) {
dernierChangement = maintenant;
etatLed = !etatLed;
digitalWrite(LED_PIN, etatLed);
}
// Ici, on pourrait ajouter d'autres taches (lecture bouton, etc.)
// sans que le clignotement ne soit bloque.
}
```
::: {.callout-note title="Pourquoi unsigned long ?"}
`millis()` renvoie une valeur qui finit par déborder (« overflow ») après environ 49 jours de fonctionnement continu. Utiliser la soustraction (maintenant - dernierChangement) plutôt qu'une comparaison directe des deux valeurs absolues gère correctement ce débordement grâce à l'arithmétique non signée.
:::
## 3. Exercice : plusieurs tâches périodiques indépendantes
Reprenez la logique de « blink sans delay » pour gérer simultanément trois comportements à des fréquences différentes : clignotement d'une LED classique toutes les 300 ms, cycle de couleur de la LED NeoPixel toutes les 1000 ms, et affichage d'un message de debug toutes les 5000 ms.
![Trois tâches périodiques indépendantes gérées avec millis(), sans jamais bloquer loop()](../schemas/svg/m06-taches-millis.svg){#fig-m06-taches-millis}
## 4. Les interruptions matérielles
### 4.1 Principe
Une interruption permet au microcontrôleur de réagir instantanément à un évènement matériel (front montant ou descendant sur une broche), en suspendant momentanément l'exécution de la boucle principale pour exécuter une fonction dédiée, appelée ISR (Interrupt Service Routine). C'est indispensable pour ne manquer aucun évènement rapide, comme une impulsion d'encodeur rotatif ou un appui bref sur un bouton.
![Déroulement d'une interruption : l'ISR lève un drapeau, loop() fait le traitement](../schemas/svg/m06-deroule-isr.svg){#fig-m06-deroule-isr}
### 4.2 `attachInterrupt()`
```cpp
#define BUTTON_PIN 5
volatile bool interruptionDetectee = false;
void IRAM_ATTR gererInterruption() {
interruptionDetectee = true;
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), gererInterruption, FALLING);
}
void loop() {
if (interruptionDetectee) {
interruptionDetectee = false;
Serial.println("Interruption traitee dans loop()");
}
}
```
::: {.callout-note title="Règles d'or d'une ISR"}
Une routine d'interruption doit être aussi courte et rapide que possible : pas de `Serial.print()`, pas de `delay()`, pas d'allocation mémoire. La bonne pratique consiste à se contenter de modifier une variable volatile (comme ci-dessus) et à traiter l'évènement dans `loop()`. Le mot-clé `IRAM_ATTR` indique que le code de la fonction doit être placé en RAM interne pour un temps d'exécution garanti, même si le cache Flash est momentanément indisponible.
:::
### 4.3 Les modes de déclenchement
| Mode | Déclenchement |
|----------|----------------------------------------|
| RISING | Sur front montant (passage de LOW à HIGH) |
| FALLING | Sur front descendant (passage de HIGH à LOW) |
| CHANGE | Sur tout changement d'état |
| LOW / HIGH | Tant que la broche reste dans cet état (peu utilisé) |
![Instants de déclenchement de l'ISR selon le mode choisi](../schemas/svg/m06-modes-declenchement.svg){#fig-m06-modes-declenchement}
## 5. Exercice : compteur d'impulsions (encodeur rotatif)
Un encodeur rotatif génère des impulsions sur deux broches (souvent nommées CLK et DT) permettant de détecter le sens de rotation. Câblez un encodeur rotatif et comptez le nombre de crans parcourus à l'aide d'une interruption sur la broche CLK.
![Encodeur rotatif : l'état de DT au front descendant de CLK donne le sens de rotation](../schemas/svg/m06-encodeur.svg){#fig-m06-encodeur}
```cpp
#define CLK_PIN 6
#define DT_PIN 7
volatile int compteur = 0;
void IRAM_ATTR gererRotation() {
if (digitalRead(DT_PIN) == HIGH) {
compteur++;
} else {
compteur--;
}
}
void setup() {
Serial.begin(115200);
pinMode(CLK_PIN, INPUT_PULLUP);
pinMode(DT_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CLK_PIN), gererRotation, FALLING);
}
void loop() {
static int dernierAffiche = 0;
if (compteur != dernierAffiche) {
dernierAffiche = compteur;
Serial.printf("Position: %d\n", compteur);
}
}
```
## 6. Les timers matériels
En complément des interruptions déclenchées par un évènement externe, l'ESP32-S3 dispose de timers matériels internes capables de générer une interruption à intervalle régulier, indépendamment de la boucle `loop()`. Utile pour des tâches critiques en temps (échantillonnage régulier, watchdog applicatif).
```cpp
hw_timer_t *minuteur = NULL;
volatile bool declenche = false;
void IRAM_ATTR onTimer() {
declenche = true;
}
void setup() {
Serial.begin(115200);
minuteur = timerBegin(1000000); // 1 MHz -> tick = 1 microseconde
timerAttachInterrupt(minuteur, &onTimer);
timerAlarm(minuteur, 1000000, true, 0); // declenchement toutes les 1 000 000 us = 1 s
}
void loop() {
if (declenche) {
declenche = false;
Serial.println("Tick timer materiel (1s)");
}
}
```
::: {.callout-note title="Version de l'API timer"}
Comme pour LEDC, l'API des timers matériels a évolué entre les versions du core Arduino-ESP32 (l'ancienne API utilisait timerBegin(numero, diviseur, compteMontant)). Vérifiez la signature correspondant à votre version installée si le code ne compile pas.
:::
## Checklist de fin de module
- [ ] Je sais remplacer un `delay()` par une gestion à base de `millis()`.
- [ ] Je sais faire cohabiter plusieurs tâches périodiques indépendantes dans `loop()`.
- [ ] Je sais déclencher une interruption avec `attachInterrupt()` et écrire une ISR minimale et sûre.
- [ ] J'ai réalisé le compteur d'impulsions avec un encodeur rotatif.
- [ ] Je comprends le rôle d'un timer matériel par rapport à une interruption externe.
+161
View File
@@ -0,0 +1,161 @@
---
title: "Module 7 — Wi-Fi : station, point d'accès et client HTTP"
---
## Objectifs du module
- Comprendre les deux modes de fonctionnement Wi-Fi de l'ESP32-S3 : station (STA) et point d'accès (AP).
- Connecter la carte à un réseau Wi-Fi existant.
- Créer son propre point d'accès Wi-Fi depuis la carte.
- Réaliser une requête HTTP GET vers une API externe et exploiter la réponse JSON.
## 1. Les modes Wi-Fi de l'ESP32-S3
| Mode | Rôle | Usage typique |
|-------------------------|----------------------------------------|----------------------------------------|
| `WIFI_STA` (Station) | La carte se connecte à un routeur/box existant | Envoyer des données sur Internet, rejoindre le réseau local |
| `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
#include <WiFi.h>
const char* ssid = "NomDuReseau";
const char* motDePasse = "MotDePasse";
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, motDePasse);
Serial.print("Connexion en cours");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connecte ! Adresse IP: ");
Serial.println(WiFi.localIP());
}
void loop() {
}
```
::: {.callout-important title="Sécurité : ne jamais versionner ses identifiants"}
Évitez d'écrire en clair vos identifiants Wi-Fi dans un fichier destiné à être partagé ou publié (dépôt Git). Une bonne pratique consiste à les isoler dans un fichier séparé non partagé (ex : secrets.h), ou à les saisir via le moniteur série au premier démarrage.
:::
## 3. Mode point d'accès : créer son propre réseau
```cpp
#include <WiFi.h>
const char* ssidAP = "ESP32-Formation";
const char* motDePasseAP = "formation123"; // 8 caracteres minimum
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_AP);
WiFi.softAP(ssidAP, motDePasseAP);
Serial.print("Point d'acces cree. IP: ");
Serial.println(WiFi.softAPIP()); // generalement 192.168.4.1
}
void loop() {
}
```
## 4. Client HTTP : interroger une API externe
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.2 Requête GET simple
```cpp
#include <WiFi.h>
#include <HTTPClient.h>
void setup() {
Serial.begin(115200);
WiFi.begin("NomDuReseau", "MotDePasse");
while (WiFi.status() != WL_CONNECTED) { delay(500); }
HTTPClient http;
http.begin("https://api.exemple.com/donnees");
int codeReponse = http.GET();
if (codeReponse == 200) {
String reponse = http.getString();
Serial.println(reponse);
} else {
Serial.printf("Erreur HTTP: %d\n", codeReponse);
}
http.end();
}
void loop() {
}
```
## 5. Exercice : afficher la météo en temps réel
Combinez cette section avec le Module 5 (écran OLED) : interrogez une API météo publique (par exemple Open-Meteo, qui ne nécessite pas de clé d'API) et affichez la température actuelle sur l'écran OLED, avec rafraîchissement toutes les 10 minutes.
```cpp
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
void setup() {
Serial.begin(115200);
WiFi.begin("NomDuReseau", "MotDePasse");
while (WiFi.status() != WL_CONNECTED) { delay(500); }
HTTPClient http;
// Exemple : latitude/longitude de Paris
http.begin("https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35&current_weather=true");
int code = http.GET();
if (code == 200) {
String payload = http.getString();
JsonDocument doc;
deserializeJson(doc, payload);
float temp = doc["current_weather"]["temperature"];
Serial.printf("Temperature actuelle: %.1f C\n", temp);
}
http.end();
}
void loop() {
}
```
::: {.callout-important title="Rappel important : ADC2 et Wi-Fi"}
Comme vu au Module 3, dès que le Wi-Fi est actif, évitez toute lecture analogique sur les broches de l'ADC2 : privilégiez systématiquement l'ADC1 dans vos projets connectés.
:::
## 6. Indicateur visuel de connexion avec la LED NeoPixel
Ajoutez un indicateur d'état de connexion sur la LED NeoPixel intégrée : orange clignotant pendant la tentative de connexion, vert fixe une fois connecté, rouge fixe en cas d'échec après un délai (par exemple 10 secondes de tentative).
## Checklist de fin de module
- [ ] Je sais connecter la carte à un réseau Wi-Fi existant (mode STA).
- [ ] Je sais créer un point d'accès Wi-Fi autonome (mode AP).
- [ ] Je sais effectuer une requête HTTP GET et lire le code de réponse.
- [ ] Je sais extraire une valeur d'une réponse JSON avec ArduinoJson.
- [ ] J'ai réalisé l'affichage météo en temps réel sur l'écran OLED.
- [ ] J'ai ajouté l'indicateur de connexion sur la LED NeoPixel.
+129
View File
@@ -0,0 +1,129 @@
---
title: "Module 8 — Serveur Web embarqué"
---
## Objectifs du module
- Comprendre le rôle d'un serveur web embarqué et son intérêt pour piloter une carte sans application dédiée.
- Créer un serveur HTTP avec WebServer.h et définir des routes.
- Construire une page HTML de contrôle simple.
- Découvrir le WebSocket pour une communication en temps réel bidirectionnelle.
## 1. Pourquoi un serveur web embarqué ?
Plutôt que de développer une application mobile dédiée, l'ESP32-S3 peut lui-même héberger une page web accessible depuis n'importe quel navigateur (smartphone, ordinateur) connecté au même réseau. C'est une solution rapide et universelle pour créer une interface de contrôle ou de supervision d'un projet.
## 2. Serveur web basique avec WebServer.h
La bibliothèque WebServer (incluse dans le core Arduino-ESP32) permet de définir des routes (URL) et la réponse associée à chacune.
```cpp
#include <WiFi.h>
#include <WebServer.h>
const char* ssid = "NomDuReseau";
const char* motDePasse = "MotDePasse";
WebServer serveur(80);
void gererRacine() {
serveur.send(200, "text/plain", "Serveur ESP32-S3 operationnel !");
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, motDePasse);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
Serial.print("Serveur accessible sur : http://");
Serial.println(WiFi.localIP());
serveur.on("/", gererRacine);
serveur.begin();
}
void loop() {
serveur.handleClient(); // A appeler en permanence, sans delay() bloquant
}
```
::: {.callout-note title="handleClient() et delay()"}
`serveur.handleClient()` doit être appelée le plus souvent possible dans `loop()`. Évitez tout `delay()` long dans votre boucle principale, sous peine de rendre le serveur peu réactif — reprenez la technique `millis()` vue au Module 6 si vous devez gérer d'autres tâches périodiques en parallèle.
:::
## 3. Page HTML de contrôle de la LED NeoPixel
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>
#include <Adafruit_NeoPixel.h>
#define NEOPIXEL_PIN 48
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
WebServer serveur(80);
const char pageHTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head><meta charset='UTF-8'>
<title>Controle LED ESP32-S3</title></head>
<body style='font-family:sans-serif;text-align:center;margin-top:50px;'>
<h1>Controle de la LED NeoPixel</h1>
<a href='/rouge'><button style='padding:15px 30px;margin:5px;'>Rouge</button></a>
<a href='/vert'><button style='padding:15px 30px;margin:5px;'>Vert</button></a>
<a href='/bleu'><button style='padding:15px 30px;margin:5px;'>Bleu</button></a>
<a href='/off'><button style='padding:15px 30px;margin:5px;'>Eteindre</button></a>
</body></html>
)rawliteral";
void changerCouleur(int r, int g, int b) {
pixel.setPixelColor(0, pixel.Color(r, g, b));
pixel.show();
}
void setup() {
pixel.begin();
WiFi.begin("NomDuReseau", "MotDePasse");
while (WiFi.status() != WL_CONNECTED) { delay(500); }
serveur.on("/", []() { serveur.send(200, "text/html", pageHTML); });
serveur.on("/rouge", []() { changerCouleur(255,0,0); serveur.send(200,"text/html",pageHTML); });
serveur.on("/vert", []() { changerCouleur(0,255,0); serveur.send(200,"text/html",pageHTML); });
serveur.on("/bleu", []() { changerCouleur(0,0,255); serveur.send(200,"text/html",pageHTML); });
serveur.on("/off", []() { changerCouleur(0,0,0); serveur.send(200,"text/html",pageHTML); });
serveur.begin();
}
void loop() {
serveur.handleClient();
}
```
::: {.callout-note title="PROGMEM"}
Le mot-clé PROGMEM place la chaîne de caractères HTML directement en mémoire Flash plutôt qu'en RAM, économisant une ressource précieuse lorsque la page grandit en taille.
:::
## 4. Introduction au WebSocket
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
En combinant les modules précédents, créez une page web affichant :
- La température et l'humidité actuelles (capteur BME280 du Module 5).
- Un bouton pour changer la couleur de la LED NeoPixel.
- L'adresse IP et le temps de fonctionnement (uptime, via `millis()`) de la carte.
## Checklist de fin de module
- [ ] Je sais créer un serveur web basique avec des routes définies.
- [ ] Je sais renvoyer une page HTML complète avec des boutons d'action.
- [ ] Je comprends la différence entre le modèle HTTP classique et le WebSocket.
- [ ] J'ai réalisé le dashboard de supervision combinant capteur, LED et informations système.
+165
View File
@@ -0,0 +1,165 @@
---
title: "Module 9 — Bluetooth Low Energy (BLE)"
---
## Objectifs du module
- Comprendre les concepts fondamentaux du BLE : serveur, service, caractéristique.
- Créer un serveur BLE sur l'ESP32-S3 exposant une caractéristique lisible et modifiable.
- Piloter la LED NeoPixel depuis une application smartphone générique (ex : nRF Connect).
- Comprendre les notifications BLE pour l'envoi de données en temps réel.
## 1. BLE sur l'ESP32-S3 : ce qu'il faut savoir
::: {.callout-warning title="Pas de Bluetooth Classic sur l'ESP32-S3"}
Contrairement à l'ESP32 « original », l'ESP32-S3 ne supporte pas le Bluetooth Classic (profil SPP notamment). Seul le Bluetooth Low Energy (BLE) est disponible. Cela signifie qu'on ne peut pas l'apparier comme un simple périphérique série Bluetooth classique : toute communication passe par le modèle GATT (services et caractéristiques) décrit dans ce module.
:::
## 2. Vocabulaire du BLE (modèle GATT)
| Terme | Définition |
|---------------|----------------------------------------|
| Serveur BLE | L'appareil qui expose des données (ici, l'ESP32-S3) |
| Client BLE | L'appareil qui se connecte et consulte les données (ex : smartphone) |
| Service | Un regroupement logique de fonctionnalités, identifié par un UUID unique |
| 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.
:::
## 3. Créer un serveur BLE minimal
La bibliothèque BLE est incluse nativement dans le core Arduino-ESP32 (aucune installation nécessaire).
```cpp
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLECharacteristic *caracteristique;
void setup() {
Serial.begin(115200);
BLEDevice::init("ESP32-S3-Formation");
BLEServer *serveur = BLEDevice::createServer();
BLEService *service = serveur->createService(SERVICE_UUID);
caracteristique = service->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE
);
caracteristique->setValue("En attente...");
service->start();
BLEDevice::getAdvertising()->start();
Serial.println("Serveur BLE demarre, en attente de connexion...");
}
void loop() {
}
```
## 4. Réagir à l'écriture d'une valeur (callback)
Pour piloter la LED NeoPixel depuis un smartphone, il faut détecter quand le client BLE écrit une nouvelle valeur dans la caractéristique.
```cpp
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <Adafruit_NeoPixel.h>
#define NEOPIXEL_PIN 48
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
class MonCallback : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *car) override {
String valeur = car->getValue();
Serial.print("Valeur recue: ");
Serial.println(valeur);
if (valeur == "rouge") pixel.setPixelColor(0, pixel.Color(255,0,0));
else if (valeur == "vert") pixel.setPixelColor(0, pixel.Color(0,255,0));
else if (valeur == "bleu") pixel.setPixelColor(0, pixel.Color(0,0,255));
else if (valeur == "off") pixel.setPixelColor(0, pixel.Color(0,0,0));
pixel.show();
}
};
void setup() {
Serial.begin(115200);
pixel.begin();
BLEDevice::init("ESP32-S3-Formation");
BLEServer *serveur = BLEDevice::createServer();
BLEService *service = serveur->createService(SERVICE_UUID);
BLECharacteristic *car = service->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE
);
car->setCallbacks(new MonCallback());
car->setValue("off");
service->start();
BLEDevice::getAdvertising()->start();
Serial.println("Pret. Connectez-vous avec nRF Connect.");
}
void loop() {
}
```
## 5. Tester avec une application smartphone
Sans développer d'application dédiée, on peut tester un serveur BLE avec une application générique :
- nRF Connect for Mobile (Nordic Semiconductor) — disponible sur Android et iOS.
- LightBlue — alternative populaire sur iOS.
### Étapes de test
- Téléverser le programme, ouvrir le moniteur série pour confirmer le démarrage.
- Ouvrir l'application, scanner les appareils BLE à proximité, repérer « ESP32-S3-Formation ».
- Se connecter, ouvrir le service correspondant à l'UUID défini.
- Sélectionner la caractéristique, utiliser la fonction d'écriture pour envoyer le texte « rouge », « vert », « bleu » ou « off ».
## 6. Les notifications : envoyer des données du serveur vers le client
À 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
// et service->createCharacteristic(..., ...)->addDescriptor(new BLE2902());
// Puis, dans loop(), pour notifier une nouvelle temperature :
String valeur = String(bme.readTemperature());
caracteristique->setValue(valeur.c_str());
caracteristique->notify();
```
## 7. Exercice combiné : télécommande BLE
Réalisez une carte pilotable entièrement en BLE : écriture pour changer la couleur de la LED NeoPixel, et notification envoyant la température lue par le BME280 (Module 5) toutes les 5 secondes vers l'application smartphone.
## Checklist de fin de module
- [ ] Je comprends la différence entre service et caractéristique dans le modèle GATT.
- [ ] Je sais créer un serveur BLE exposant une caractéristique en lecture/écriture.
- [ ] J'ai testé la communication avec une application smartphone générique.
- [ ] Je comprends le rôle des notifications BLE.
- [ ] J'ai réalisé la télécommande BLE combinant écriture et notification.
+118
View File
@@ -0,0 +1,118 @@
---
title: "Module 10 — Gestion de l'énergie"
---
## Objectifs du module
- Comprendre les enjeux de consommation d'un projet ESP32-S3 alimenté sur batterie.
- Distinguer les différents modes de veille (modem-sleep, light sleep, deep sleep).
- Mettre en œuvre un réveil programmé (timer) et un réveil sur évènement externe (GPIO).
- Concevoir un capteur autonome optimisé pour l'autonomie sur batterie.
## 1. Pourquoi se soucier de la consommation ?
Un ESP32-S3 alimenté en continu (Wi-Fi actif, calcul permanent) consomme couramment 100 à 250 mA. Sur batterie, cela vide une pile de 2000 mAh en moins d'une journée. Pour un capteur autonome destiné à fonctionner plusieurs mois sur pile, il faut exploiter les modes de veille pour ne consommer significativement que pendant les brefs instants où la carte est réellement active.
## 2. Les différents modes de veille
| Mode | Consommation typique | État conservé | Réveil possible via |
|--------------|------------------------|----------------------------------------|--------------------------------|
| Actif (normal) | 80-250 mA | Tout | — |
| Modem-sleep | \~20-30 mA | RAM, CPU actif par intermittence | Automatique (Wi-Fi/BT en veille) |
| Light sleep | \~0,8 mA | RAM et état conservés | Timer, GPIO, UART... |
| Deep sleep | \~10-150 µA | RTC RAM uniquement, redémarrage du programme | Timer, GPIO (EXT0/EXT1) |
![Consommation selon le mode et autonomie correspondante sur une batterie de 2000 mAh (ordres de grandeur)](../schemas/svg/m10-consommation-modes.svg){#fig-m10-consommation-modes}
::: {.callout-note title="Le deep sleep redémarre le programme"}
Contrairement au light sleep, sortir du deep sleep provoque un redémarrage complet du programme (comme après un reset) : `setup()` est ré-exécuté depuis le début. Seule la zone mémoire RTC (déclarée avec `RTC_DATA_ATTR`) est préservée entre deux cycles de deep sleep, ce qui permet de conserver un compteur ou un état minimal.
:::
## 3. Deep sleep avec réveil par timer
Cas d'usage typique : un capteur qui se réveille toutes les X minutes, effectue une mesure, l'envoie, puis se rendort.
![Profil de courant d'un capteur en deep sleep réveillé toutes les 60 s (échelle logarithmique)](../schemas/svg/m10-cycle-deep-sleep.svg){#fig-m10-cycle-deep-sleep}
```cpp
#define TEMPS_REVEIL_SECONDES 60
RTC_DATA_ATTR int compteurReveils = 0; // conserve sa valeur entre deux deep sleep
void setup() {
Serial.begin(115200);
delay(200);
compteurReveils++;
Serial.printf("Reveil numero %d\n", compteurReveils);
// ... effectuer ici la mesure, l'envoi de donnees, etc. ...
Serial.println("Retour en deep sleep.");
esp_sleep_enable_timer_wakeup(TEMPS_REVEIL_SECONDES * 1000000ULL); // en microsecondes
esp_deep_sleep_start();
}
void loop() {
// Jamais atteint : le deep sleep redemarre au setup()
}
```
## 4. Deep sleep avec réveil sur GPIO (bouton)
Autre cas d'usage : une carte qui reste endormie jusqu'à ce qu'un utilisateur appuie sur un bouton.
```cpp
#define BOUTON_REVEIL GPIO_NUM_4
void setup() {
Serial.begin(115200);
delay(200);
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
if (cause == ESP_SLEEP_WAKEUP_EXT0) {
Serial.println("Reveil provoque par le bouton.");
} else {
Serial.println("Premier demarrage (pas un reveil).");
}
// ... traitement ...
esp_sleep_enable_ext0_wakeup(BOUTON_REVEIL, 0); // reveil sur niveau LOW
Serial.println("Deep sleep, en attente d'un appui bouton...");
esp_deep_sleep_start();
}
void loop() {
}
```
::: {.callout-note title="Broche compatible RTC obligatoire"}
Le réveil par GPIO (EXT0/EXT1) en deep sleep nécessite d'utiliser une broche appartenant au domaine RTC du microcontrôleur. Toutes les broches ne sont pas éligibles : vérifiez la documentation de votre référence de carte pour identifier les GPIO compatibles RTC.
:::
## 5. Bonnes pratiques pour un projet sur batterie
- Couper le Wi-Fi/BLE dès que la transmission de données est terminée (`WiFi.disconnect(true)`; `WiFi.mode(WIFI_OFF)`;).
- Réduire la fréquence CPU si le calcul n'est pas critique en performance (`setCpuFrequencyMhz()`).
- Limiter le temps d'éveil au strict nécessaire : mesurer, envoyer, dormir.
- Éviter les composants annexes qui consomment en continu (LED témoin, capteur non coupé) — prévoir une alimentation commutée si besoin (MOSFET de coupure).
- Pour la LED NeoPixel : elle consomme significativement plus qu'une LED simple (jusqu'à 60 mA à pleine luminosité blanche) — la couper explicitement (couleur noire) avant le deep sleep, et limiter sa luminosité (setBrightness) pour un usage sur batterie.
## 6. Exercice : capteur météo autonome
En combinant les modules précédents, réalisez un capteur qui :
- Se réveille toutes les 5 minutes via un timer deep sleep.
- Lit la température et l'humidité (BME280, Module 5).
- Affiche un court flash de couleur sur la LED NeoPixel selon la température (bleu/vert/rouge, comme au Module 5), puis l'éteint.
- Retourne en deep sleep.
- Affiche dans le moniteur série, à chaque réveil, le nombre total de réveils depuis la mise sous tension (compteur en `RTC_DATA_ATTR`).
## Checklist de fin de module
- [ ] Je sais différencier modem-sleep, light sleep et deep sleep.
- [ ] Je sais programmer un réveil par timer en deep sleep.
- [ ] Je sais programmer un réveil par GPIO en deep sleep, sur une broche compatible RTC.
- [ ] Je connais au moins trois bonnes pratiques pour réduire la consommation d'un projet sur batterie.
- [ ] J'ai réalisé le capteur météo autonome en deep sleep.
+177
View File
@@ -0,0 +1,177 @@
---
title: "Module 11 — IoT & Cloud : le protocole MQTT"
---
## Objectifs du module
- Comprendre le principe du protocole MQTT (publish/subscribe) et son intérêt en IoT.
- Connecter l'ESP32-S3 à un broker MQTT.
- Publier des données de capteurs et s'abonner à des commandes.
- Visualiser les données dans un tableau de bord (Node-RED ou Home Assistant).
## 1. Pourquoi MQTT plutôt qu'HTTP pour l'IoT ?
Le modèle HTTP vu au Module 7 fonctionne bien pour des requêtes ponctuelles, mais devient lourd pour des objets connectés qui doivent échanger fréquemment de petits messages (température toutes les secondes, changement d'état instantané). MQTT (Message Queuing Telemetry Transport) est un protocole léger conçu spécifiquement pour l'IoT, basé sur un modèle publish/subscribe (publication/abonnement) plutôt que sur des requêtes/réponses.
## 2. Le modèle publish/subscribe
| Élément | Rôle |
|-------------|----------------------------------------|
| Broker | Serveur central qui reçoit les messages publiés et les redistribue aux abonnés (ex : Mosquitto, HiveMQ) |
| Topic (sujet) | Chaîne hiérarchique identifiant un flux de données (ex : maison/salon/temperature) |
| Publish | Envoyer un message sur un topic donné |
| Subscribe | S'abonner à un topic pour recevoir tous les messages publiés dessus |
![Les topics forment une arborescence, comme des chemins de fichiers](../schemas/svg/m11-arborescence-topics.svg){#fig-m11-arborescence-topics width=90%}
Avantage clé : les appareils ne communiquent jamais directement entre eux — tout transite par le broker, ce qui simplifie grandement l'architecture (un capteur ne connaît pas l'adresse du tableau de bord qui affichera ses données, il publie simplement sur un topic).
![Modèle publish/subscribe : la carte publie ses mesures et reçoit ses commandes via le broker](../schemas/svg/m11-publish-subscribe.svg){#fig-m11-publish-subscribe}
## 3. Choisir un broker MQTT
- Mosquitto : broker open-source, installable localement (Raspberry Pi, PC, serveur domestique) ou utilisable en test public (test.mosquitto.org).
- HiveMQ Cloud : offre gratuite hébergée, pratique pour démarrer sans installation.
- Le broker intégré à Home Assistant : pratique si vous utilisez déjà cette plateforme domotique.
::: {.callout-important title="Sécurité en production"}
Pour un usage de test/formation, un broker public non sécurisé (comme test.mosquitto.org) est acceptable. Pour un déploiement réel, utilisez toujours une connexion chiffrée (MQTTS, port 8883) avec authentification par identifiants ou certificat.
:::
## 4. Connexion MQTT avec PubSubClient
Installez la bibliothèque « PubSubClient » (Nick O'Leary) depuis le gestionnaire de bibliothèques.
```cpp
#include <WiFi.h>
#include <PubSubClient.h>
const char* ssid = "NomDuReseau";
const char* motDePasse = "MotDePasse";
const char* brokerMQTT = "test.mosquitto.org";
const int portMQTT = 1883;
WiFiClient espClient;
PubSubClient client(espClient);
void connecterMQTT() {
while (!client.connected()) {
Serial.print("Connexion au broker MQTT...");
String idClient = "ESP32S3-" + String(random(0xffff), HEX);
if (client.connect(idClient.c_str())) {
Serial.println(" connecte !");
} else {
Serial.printf(" echec, code erreur: %d. Nouvelle tentative dans 2s.\n", client.state());
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, motDePasse);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
client.setServer(brokerMQTT, portMQTT);
connecterMQTT();
}
void loop() {
if (!client.connected()) connecterMQTT();
client.loop(); // A appeler en permanence, comme handleClient() pour le serveur web
}
```
## 5. Publier des données de capteur
```cpp
unsigned long dernierEnvoi = 0;
const unsigned long intervalle = 5000;
void loop() {
if (!client.connected()) connecterMQTT();
client.loop();
unsigned long maintenant = millis();
if (maintenant - dernierEnvoi >= intervalle) {
dernierEnvoi = maintenant;
float temperature = bme.readTemperature();
char message[10];
dtostrf(temperature, 4, 1, message); // conversion float -> texte
client.publish("formation/esp32s3/temperature", message);
Serial.printf("Publie: %s\n", message);
}
}
```
## 6. S'abonner à un topic (recevoir des commandes)
Réutilisons la LED NeoPixel : elle pourra être pilotée à distance en s'abonnant à un topic de commande.
```cpp
#include <Adafruit_NeoPixel.h>
#define NEOPIXEL_PIN 48
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
void callbackMQTT(char* topic, byte* payload, unsigned int longueur) {
String message;
for (unsigned int i = 0; i < longueur; i++) message += (char)payload[i];
Serial.printf("Message recu sur %s: %s\n", topic, message.c_str());
if (message == "rouge") pixel.setPixelColor(0, pixel.Color(255,0,0));
else if (message == "vert") pixel.setPixelColor(0, pixel.Color(0,255,0));
else if (message == "bleu") pixel.setPixelColor(0, pixel.Color(0,0,255));
else if (message == "off") pixel.setPixelColor(0, pixel.Color(0,0,0));
pixel.show();
}
void connecterMQTT() {
while (!client.connected()) {
String idClient = "ESP32S3-" + String(random(0xffff), HEX);
if (client.connect(idClient.c_str())) {
client.subscribe("formation/esp32s3/commande");
} else {
delay(2000);
}
}
}
void setup() {
pixel.begin();
// ... connexion WiFi ...
client.setServer(brokerMQTT, portMQTT);
client.setCallback(callbackMQTT);
connecterMQTT();
}
```
## 7. Visualiser les données : Node-RED ou Home Assistant
Une fois les données publiées sur le broker, il est possible de les visualiser sans écrire de code supplémentaire :
- Node-RED : outil de programmation visuelle par flux, avec un nœud MQTT natif et un tableau de bord (dashboard) configurable en quelques clics.
- Home Assistant : plateforme domotique qui peut découvrir automatiquement les topics MQTT et générer des cartes de suivi (courbes de température, boutons de commande).
::: {.callout-note title="Portée pédagogique"}
La mise en place détaillée de Node-RED ou Home Assistant dépasse le cadre strict de la programmation Arduino/ESP32 ; ce module se concentre sur la partie « objet connecté » (ESP32-S3). L'installation et la configuration de l'outil de visualisation pourront faire l'objet d'un support complémentaire selon les besoins du groupe.
:::
## 8. Exercice final du module : station météo connectée
En combinant tous les modules précédents, réalisez une station météo IoT complète :
- Lecture température/humidité via le BME280 (Module 5).
- Publication MQTT toutes les 30 secondes sur un topic dédié.
- Abonnement à un topic de commande pilotant la couleur de la LED NeoPixel.
- Affichage local sur l'écran OLED de la dernière valeur publiée et de l'état de connexion MQTT.
## Checklist de fin de module
- [ ] Je comprends le modèle publish/subscribe et le rôle du broker.
- [ ] Je sais connecter l'ESP32-S3 à un broker MQTT.
- [ ] Je sais publier une donnée de capteur sur un topic.
- [ ] Je sais m'abonner à un topic et réagir à un message reçu.
- [ ] J'ai réalisé la station météo connectée combinant capteur, MQTT, OLED et LED NeoPixel.
+89
View File
@@ -0,0 +1,89 @@
---
title: "Module 12 — Projet final"
---
## Objectifs du module
- Mobiliser l'ensemble des compétences acquises (GPIO, PWM, ADC, I2C, interruptions, Wi-Fi, BLE, énergie, MQTT) dans un projet personnel cohérent.
- Rédiger un cahier des charges simple avant de coder.
- Structurer un programme Arduino de taille conséquente (plusieurs fichiers, fonctions bien séparées).
- Présenter et démontrer son projet.
## 1. Méthodologie : avant de coder
Un projet réussi commence par une phase de cadrage, même brève. Avant d'ouvrir l'IDE, répondez par écrit aux questions suivantes :
- Quel problème mon projet résout-il, ou quel besoin satisfait-il ?
- Quelles sont les fonctionnalités indispensables (MVP - produit minimum viable) et lesquelles sont « bonus » si le temps le permet ?
- Quel matériel est nécessaire (capteurs, actionneurs, alimentation) et est-il disponible ?
- Le projet doit-il fonctionner de façon autonome (batterie) ou reste-t-il branché en continu ?
- Quels modules de la formation seront mobilisés ?
![Démarche de projet : du besoin à la présentation](../schemas/svg/m12-demarche-projet.svg){#fig-m12-demarche-projet}
## 2. Exemples de projets (par ordre de complexité croissante)
| Projet | Modules mobilisés | Description |
|------------------------------|---------------------|----------------------------------------|
| Station météo de bureau | 1, 2, 3, 5 | Écran OLED affichant température/humidité locales, LED NeoPixel comme indicateur de confort |
| Station météo connectée | 5, 7 ou 11 | La précédente, mais publiant les données sur Internet (API ou MQTT) et consultable à distance |
| Capteur autonome sur batterie | 5, 10, 11 | Mesure périodique avec deep sleep, transmission MQTT, autonomie de plusieurs semaines |
| Domotique locale | 2, 3, 8 | Serveur web pilotant plusieurs relais/LED, avec retour d'état en temps réel |
| Télécommande BLE personnalisée | 6, 9 | Boîtier avec boutons/encodeur pilotant un appareil via BLE (ex : diaporama, musique) |
| Jeu de réflexes lumineux | 2, 4, 6 | Jeu type « Simon » utilisant plusieurs LED (ou la NeoPixel en RGB) et un buzzer, avec score affiché en série |
::: {.callout-note title="Un projet libre est aussi possible"}
Cette liste est indicative : n'importe quel projet personnel combinant au moins trois modules de la formation et incluant une utilisation réfléchie de la LED NeoPixel est recevable.
:::
## 3. Cahier des charges type (à compléter)
| Rubrique | À compléter |
|----------------------------------------|---------------|
| Nom du projet | |
| Objectif en une phrase | |
| Fonctionnalités indispensables | |
| Fonctionnalités bonus (si le temps le permet) | |
| Matériel nécessaire | |
| Modules de la formation mobilisés | |
| Contraintes (autonomie, taille, coût...) | |
## 4. Structurer un projet de taille conséquente
Au-delà d'une centaine de lignes, un seul fichier .ino devient difficile à maintenir. L'IDE Arduino permet de scinder un croquis en plusieurs onglets/fichiers, automatiquement inclus à la compilation :
- MonProjet.ino : contient uniquement `setup()` et `loop()`, orchestrant les appels aux autres fonctions.
- capteurs.h / capteurs.cpp (ou simplement un onglet capteurs.ino) : fonctions de lecture et traitement des capteurs.
- affichage.ino : fonctions liées à l'écran OLED et à la LED NeoPixel.
- reseau.ino : fonctions Wi-Fi/MQTT/BLE.
![Organisation d'un croquis en plusieurs onglets, une responsabilité par fichier](../schemas/svg/m12-structure-projet.svg){#fig-m12-structure-projet}
::: {.callout-tip title="Bonne pratique de structuration"}
Une fonction doit avoir une seule responsabilité claire (ex : `lireTemperature()`, `afficherEcran()`, `publierMQTT()`). Cette organisation facilite grandement le débogage et la relecture, surtout en formation collective où le code peut être partagé et commenté entre apprenants.
:::
## 5. Grille d'auto-évaluation avant présentation
| Critère | Validé ? |
|----------------------------------------|------------|
| Le projet répond au besoin défini dans le cahier des charges | ☐ |
| Le code compile sans erreur ni avertissement bloquant | ☐ |
| Les fonctions sont nommées clairement et bien séparées | ☐ |
| Le programme gère les cas d'erreur (capteur non détecté, Wi-Fi indisponible...) | ☐ |
| La LED NeoPixel est utilisée de façon pertinente (état, alerte, feedback) | ☐ |
| Le projet a été testé sur une durée représentative de son usage réel | ☐ |
| Une courte documentation (schéma de câblage, liste du matériel) accompagne le code | ☐ |
## 6. Présentation du projet
Chaque participant présente son projet en quelques minutes :
- Démonstration du fonctionnement réel sur la carte.
- Explication des choix techniques (pourquoi ce protocole, cette architecture).
- Difficultés rencontrées et solutions apportées.
- Pistes d'amélioration si le projet devait être poursuivi.
## Conclusion de la formation
Ce parcours a couvert la prise en main du matériel, les entrées/sorties, les signaux analogiques, les bus de communication, la gestion du temps et des évènements, la connectivité Wi-Fi et BLE, la gestion de l'énergie et l'IoT via MQTT — un socle de compétences complet pour aborder de façon autonome tout projet basé sur l'ESP32-S3 avec le framework Arduino. La suite naturelle consiste à approfondir un domaine particulier selon vos projets futurs : traitement du signal, IA embarquée (TinyML), protocoles radio longue portée (LoRa), ou intégration à des écosystèmes domotiques existants.
+83
View File
@@ -0,0 +1,83 @@
---
title: "Synthèse de la formation & liste de matériel"
---
## 1. Vue d'ensemble des modules
![Le parcours de la formation : cinq étapes qui convergent vers le projet final](../schemas/svg/m13-parcours-formation.svg){#fig-m13-parcours-formation}
| Module | Titre | Compétences clés |
|----------|-----------------------------|----------------------------------------|
| 0 | Prérequis & Installation | Arduino IDE, pilotes USB, ajout du support ESP32, premier Blink |
| 1 | Découverte du microcontrôleur | Architecture ESP32-S3, différences vs Arduino Uno, lecture du pinout |
| 2 | GPIO & LED NeoPixel | Entrées/sorties numériques, anti-rebond, pilotage de la LED RGB intégrée |
| 3 | PWM & ADC | Variateur de luminosité, lecture analogique, potentiomètre |
| 4 | Communication série & debug | UART, ports série multiples, console de commandes |
| 5 | Bus I2C et SPI | Scanner I2C, écran OLED, capteur BME280, comparatif I2C/SPI |
| 6 | Interruptions & Timers | `millis()`, `attachInterrupt()`, encodeur rotatif, timers matériels |
| 7 | Wi-Fi | Mode station/point d'accès, requêtes HTTP, API météo |
| 8 | Serveur Web embarqué | WebServer.h, page HTML de contrôle, notion de WebSocket |
| 9 | Bluetooth Low Energy | Serveur BLE, services/caractéristiques, notifications |
| 10 | Gestion de l'énergie | Modes de veille, deep sleep, réveil timer/GPIO, capteur autonome |
| 11 | IoT & MQTT | Broker MQTT, publish/subscribe, station météo connectée |
| 12 | Projet final | Cahier des charges, structuration de projet, présentation |
## 2. Liste de matériel consolidée (à acheter)
Cette liste regroupe l'ensemble des composants nécessaires pour réaliser tous les exercices de la formation. Les quantités indiquées correspondent à un poste de travail (un apprenant ou un binôme).
### 2.1 Carte et informatique
| Article | Quantité | Modules concernés | Remarques |
|----------------------------------------|------------|---------------------|----------------------------------------|
| Carte ESP32-S3-DevKitC-1 (WROOM-1) | 1 | Tous | Vérifier la présence d'une LED NeoPixel intégrée et la référence exacte du pinout |
| Câble USB-C (données, pas uniquement charge) | 1 | Tous | Indispensable pour la programmation et l'alimentation |
| Ordinateur avec Arduino IDE installé | 1 | Tous | Voir Module 0 pour l'installation |
### 2.2 Composants électroniques de base
| Article | Quantité | Modules concernés | Remarques |
|----------------------------------------|------------|------------------------------|-------------------------------------|
| Breadboard (plaque d'essai) 830 points | 1 | 2, 3, 4, 5, 6 | Support de câblage sans soudure |
| Kit de fils de câblage (jumpers M-M, M-F) | 1 kit | Tous les TP câblés | Prévoir mâle-mâle et mâle-femelle |
| LED 5 mm (couleurs variées) | 4 à 6 | Module 2 (chenillard) | Rouge, vert, bleu, jaune conseillées |
| Résistances 220 Ω | 6 | Module 2 | Protection des LED |
| Résistances 10 kΩ | 2 | Complément pull-down si besoin | Facultatif selon montage |
| Bouton-poussoir (push-button) 4 pattes | 2 | Modules 2, 4, 6 | Un de rechange conseillé |
| Potentiomètre rotatif 10 kΩ | 1 | Module 3 | Avec bouton de préhension si possible |
### 2.3 Modules et capteurs
| Article | Quantité | Modules concernés | Remarques |
|----------------------------------------|------------|---------------------|----------------------------------------|
| Écran OLED I2C SSD1306 0,96" (128x64) | 1 | 5, 7, 8, 11 | Vérifier alimentation 3,3V compatible |
| Capteur BME280 (température/humidité/pression), I2C | 1 | 5, 10, 11 | Attention aux clones BMP280 (pas d'humidité) : bien vérifier la référence BME280 |
| Encodeur rotatif avec bouton intégré (type KY-040) | 1 | 6 | Génère 2 signaux CLK/DT + bouton poussoir |
### 2.4 Alimentation et énergie (Module 10 et projets autonomes)
| Article | Quantité | Modules concernés | Remarques |
|----------------------------------------|------------|-----------------------|----------------------------------------|
| Pile ou batterie externe (powerbank, ou support piles AA/LiPo) | 1 | Module 10, projet final | Selon le projet choisi ; un support 2xAA avec câble JST suffit pour les tests |
| Câble/connecteur adapté à l'alimentation choisie | 1 | Module 10 | Vérifier compatibilité avec l'entrée d'alimentation de la carte |
### 2.5 Facultatif / selon projet final
Selon le projet choisi par chaque apprenant au Module 12, du matériel additionnel peut être nécessaire : relais, buzzer, servomoteur, capteur supplémentaire (PIR, ultrason...), boîtier d'impression 3D, etc. Ce matériel n'est pas listé ici car dépendant du projet individuel — à définir une fois le cahier des charges rédigé (voir Module 12, section 3).
::: {.callout-note title="Application smartphone (gratuite, pas d'achat)"}
Pour le Module 9 (BLE), chaque apprenant doit disposer d'un smartphone avec l'application gratuite « nRF Connect for Mobile » (Android/iOS) ou équivalent (LightBlue sur iOS). Aucun matériel supplémentaire à acheter pour ce module.
:::
## 3. Tableau récapitulatif quantités globales
| Catégorie | Nombre de références distinctes |
|--------------------------------|-----------------------------------|
| Carte et informatique | 3 |
| Composants électroniques de base | 7 |
| Modules et capteurs | 3 |
| Alimentation / énergie | 2 |
::: {.callout-tip title="Astuce achat groupé"}
La plupart de ces composants (hors carte ESP32-S3 et écran OLED/BME280) sont généralement disponibles dans un kit de démarrage Arduino/ESP32 générique, ce qui peut s'avérer plus économique que des achats à l'unité pour un groupe de plusieurs apprenants.
:::
+1 -1
View File
@@ -29,7 +29,7 @@ def modules_figures(filtres=()):
def enregistrer(fig, id_):
if hasattr(fig, "fig"): # dessin schemdraw (backend matplotlib)
fig = fig.fig
fig.savefig(DOSSIER_SVG / f"{id_}.svg")
fig.savefig(DOSSIER_SVG / f"{id_}.svg", metadata={"Date": None}) # pas de date : diff git stable
fig.savefig(DOSSIER_PNG / f"{id_}.png", dpi=200)
plt.close(fig)
-172
View File
@@ -1,172 +0,0 @@
"""Insère les schémas générés dans les supports .odt.
Usage : python schemas/inserer.py [Module_03_PWM_ADC.odt ...]
Sans argument : traite chaque support pour lequel un fichier mXX_*.py existe.
Idempotent : les figures déjà insérées (cadres « schema-* ») sont retirées puis réinsérées.
"""
import os
import re
import struct
import sys
import tempfile
import zipfile
from pathlib import Path
from lxml import etree
from generer import DOSSIER_PNG, DOSSIER_SVG, modules_figures
RACINE = Path(__file__).resolve().parent.parent
NS = {
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
"draw": "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
"svg": "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
"xlink": "http://www.w3.org/1999/xlink",
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
}
STYLE_IMAGE = "Schema_Image"
STYLE_LEGENDE = "Schema_Legende"
STYLE_CADRE = "Schema_Cadre"
def q(nom):
prefixe, local = nom.split(":")
return f"{{{NS[prefixe]}}}{local}"
def normaliser(texte):
return re.sub(r"\s+", " ", texte.replace("’", "'")).strip()
def taille_png(chemin):
with open(chemin, "rb") as f:
entete = f.read(24)
return struct.unpack(">II", entete[16:24])
def ajouter_styles(racine):
auto = racine.find(q("office:automatic-styles"))
existants = {s.get(q("style:name")) for s in auto}
if STYLE_IMAGE not in existants:
s = etree.SubElement(auto, q("style:style"), {q("style:name"): STYLE_IMAGE, q("style:family"): "paragraph"})
etree.SubElement(s, q("style:paragraph-properties"), {
q("fo:text-align"): "center", q("fo:margin-top"): "0.3cm",
q("fo:margin-bottom"): "0.1cm", q("fo:keep-with-next"): "always"})
if STYLE_LEGENDE not in existants:
s = etree.SubElement(auto, q("style:style"), {q("style:name"): STYLE_LEGENDE, q("style:family"): "paragraph"})
etree.SubElement(s, q("style:paragraph-properties"), {
q("fo:text-align"): "center", q("fo:margin-top"): "0cm", q("fo:margin-bottom"): "0.4cm"})
etree.SubElement(s, q("style:text-properties"), {
q("fo:font-style"): "italic", q("fo:font-size"): "9pt", q("fo:color"): "#4b5563"})
if STYLE_CADRE not in existants:
s = etree.SubElement(auto, q("style:style"), {q("style:name"): STYLE_CADRE, q("style:family"): "graphic"})
etree.SubElement(s, q("style:graphic-properties"), {
q("style:vertical-pos"): "top", q("style:vertical-rel"): "baseline",
q("fo:border"): "none", q("fo:padding"): "0cm"})
def retirer_figures(corps):
styles = (STYLE_IMAGE, STYLE_LEGENDE)
for p in list(corps.iter(q("text:p"))):
if p.get(q("text:style-name")) in styles:
p.getparent().remove(p)
def trouver_point_insertion(corps, ancre):
"""Renvoie l'élément après lequel insérer : le paragraphe d'introduction qui suit le titre."""
cible = normaliser(ancre)
for p in corps.iter(q("text:p"), q("text:h")):
if normaliser("".join(p.itertext())) == cible:
suivant = p.getnext()
while suivant is not None and suivant.tag == q("text:p") and not "".join(suivant.itertext()).strip():
suivant = suivant.getnext()
if suivant is not None and suivant.tag == q("text:p"):
return suivant
return p
raise ValueError(f"ancre introuvable : {ancre!r}")
def construire_figure(f, numero):
largeur, hauteur = taille_png(DOSSIER_PNG / f"{f.id}.png")
h_cm = f.largeur_cm * hauteur / largeur
p_image = etree.Element(q("text:p"), {q("text:style-name"): STYLE_IMAGE})
cadre = etree.SubElement(p_image, q("draw:frame"), {
q("draw:style-name"): STYLE_CADRE, q("draw:name"): f"schema-{f.id}",
q("text:anchor-type"): "as-char", q("svg:width"): f"{f.largeur_cm:.2f}cm",
q("svg:height"): f"{h_cm:.2f}cm", q("draw:z-index"): "0"})
for ext, mime in (("svg", "image/svg+xml"), ("png", "image/png")):
etree.SubElement(cadre, q("draw:image"), {
q("xlink:href"): f"Pictures/{f.id}.{ext}", q("xlink:type"): "simple",
q("xlink:show"): "embed", q("xlink:actuate"): "onLoad", q("draw:mime-type"): mime})
etree.SubElement(cadre, q("svg:title")).text = f.legende
p_legende = etree.Element(q("text:p"), {q("text:style-name"): STYLE_LEGENDE})
p_legende.text = f"Figure {numero} — {f.legende}"
return p_image, p_legende
def maj_manifest(xml, figures):
racine = etree.fromstring(xml)
for e in list(racine):
if e.get(q("manifest:full-path"), "").startswith("Pictures/schema-") or \
re.match(r"Pictures/m\d\d-", e.get(q("manifest:full-path"), "")):
racine.remove(e)
for f in figures:
for ext, mime in (("svg", "image/svg+xml"), ("png", "image/png")):
etree.SubElement(racine, q("manifest:file-entry"), {
q("manifest:full-path"): f"Pictures/{f.id}.{ext}", q("manifest:media-type"): mime})
return etree.tostring(racine, xml_declaration=True, encoding="UTF-8")
def traiter(odt, figures):
num_module = int(figures[0].id[1:3])
with zipfile.ZipFile(odt) as z:
entrees = {i.filename: z.read(i.filename) for i in z.infolist()}
ordre = [i.filename for i in z.infolist()]
racine = etree.fromstring(entrees["content.xml"])
corps = racine.find(f"{q('office:body')}/{q('office:text')}")
ajouter_styles(racine)
retirer_figures(corps)
for n, f in enumerate(figures, 1):
point = trouver_point_insertion(corps, f.ancre)
p_image, p_legende = construire_figure(f, f"{num_module}.{n}")
point.addnext(p_image)
p_image.addnext(p_legende)
entrees["content.xml"] = etree.tostring(racine, xml_declaration=True, encoding="UTF-8")
entrees["META-INF/manifest.xml"] = maj_manifest(entrees["META-INF/manifest.xml"], figures)
# Images : on retire les anciennes versions puis on ajoute les nouvelles
ordre = [n for n in ordre if not re.match(r"Pictures/m\d\d-", n)]
for f in figures:
entrees[f"Pictures/{f.id}.svg"] = (DOSSIER_SVG / f"{f.id}.svg").read_bytes()
entrees[f"Pictures/{f.id}.png"] = (DOSSIER_PNG / f"{f.id}.png").read_bytes()
ordre += [f"Pictures/{f.id}.svg", f"Pictures/{f.id}.png"]
# Réécriture : mimetype en premier et non compressé (exigence ODF)
fd, temp = tempfile.mkstemp(suffix=".odt", dir=odt.parent)
os.close(fd)
with zipfile.ZipFile(temp, "w") as z:
z.writestr(zipfile.ZipInfo("mimetype"), entrees["mimetype"], compress_type=zipfile.ZIP_STORED)
for nom in ordre:
if nom != "mimetype":
z.writestr(nom, entrees[nom], compress_type=zipfile.ZIP_DEFLATED)
os.replace(temp, odt)
print(f"{odt.name} : {len(figures)} figure(s) insérée(s)")
def main():
par_module = modules_figures()
cibles = [RACINE / a for a in sys.argv[1:]] or \
[p for p in sorted(RACINE.glob("Module_*.odt")) if f"m{p.name[7:9]}" in par_module]
for odt in cibles:
prefixe = f"m{odt.name[7:9]}"
if prefixe not in par_module:
print(f"{odt.name} : aucune figure déclarée, ignoré")
continue
traiter(odt, par_module[prefixe])
if __name__ == "__main__":
main()
+53
View File
@@ -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),
]
+85
View File
@@ -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),
]
+121
View File
@@ -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),
]
+4 -12
View File
@@ -138,16 +138,8 @@ def adc_quantification():
FIGURES = [
Figure("m03-pwm-rapport-cyclique",
"Le rapport cyclique fixe la tension moyenne perçue par la charge (LED, moteur)",
"1. Qu'est-ce que le PWM ?", pwm_rapport_cyclique),
Figure("m03-servo-50hz",
"Commande d'un servomoteur : la largeur de l'impulsion (1 à 2 ms) fixe l'angle",
"2.2 Approche explicite : API LEDC (contrôle fin)", servo_50hz, largeur_cm=15),
Figure("m03-montage-variateur",
"Câblage de l'exercice : potentiomètre sur GPIO1 (ADC1), LED sur GPIO4 (PWM)",
"3. Exercice : variateur de luminosité avec potentiomètre", montage_variateur, largeur_cm=14),
Figure("m03-adc-quantification",
"Conversion analogique-numérique puis mise à l'échelle avec map()",
"4. Le convertisseur analogique-numérique (ADC)", adc_quantification),
Figure("m03-pwm-rapport-cyclique", pwm_rapport_cyclique),
Figure("m03-servo-50hz", servo_50hz),
Figure("m03-montage-variateur", montage_variateur),
Figure("m03-adc-quantification", adc_quantification),
]
+63
View File
@@ -0,0 +1,63 @@
"""Module 4 — Communication série & débogage."""
from outils import boite, chronogramme, fleche, toile
from style import BLEU, ENCRE, GRIS, ORANGE, VERT, Figure
def trame_uart():
# 'A' = 0x41 = 0b01000001, envoyé bit de poids faible en premier
donnees = [(0x41 >> k) & 1 for k in range(8)]
bits = [1] + [0] + donnees + [1] + [1] # repos, start, D0..D7, stop, repos
points = [(i, b) for i, b in enumerate(bits)]
fig, ax, y = chronogramme([("TX", points)], 12, largeur_cm=16, hauteur_voie_cm=2.2)
ax.set_xticks([])
ax.spines["bottom"].set_visible(False)
noms = ["repos", "start"] + [f"D{k}" for k in range(8)] + ["stop", "repos"]
couleurs = [GRIS, ORANGE] + [BLEU] * 8 + [VERT, GRIS]
for i, (n, c) in enumerate(zip(noms, couleurs)):
ax.text(i + 0.5, 1.35, n, ha="center", fontsize=8, color=c, fontweight="bold")
if 2 <= i <= 9:
ax.text(i + 0.5, -0.35, str(bits[i]), ha="center", fontsize=9, color=ENCRE)
ax.axvline(i, ymin=0.12, ymax=0.83, color="#e5e7eb", lw=0.7, zorder=0)
ax.annotate("", xy=(2, 1.85), xytext=(10, 1.85), arrowprops=dict(arrowstyle="<->", lw=0.9, color=BLEU))
ax.text(6, 1.95, "8 bits de données : 'A' = 0x41, bit de poids faible (D0) en premier",
ha="center", va="bottom", fontsize=8, color=BLEU)
ax.annotate("", xy=(1, -0.75), xytext=(2, -0.75), arrowprops=dict(arrowstyle="<->", lw=0.9))
ax.text(2.15, -0.75, "1 bit = 1 / 115200 s ≈ 8,7 µs", va="center", fontsize=8, color=ENCRE)
ax.text(12, -0.75, "8N1 : 8 bits, pas de parité, 1 bit de stop", ha="right", va="center",
fontsize=8, color=GRIS)
ax.set_ylim(-1.1, 2.5)
fig.tight_layout()
return fig
def cablage_serie():
fig, ax = toile(16, 6.8)
pc = boite(ax, 1.8, 3.1, 3.0, 2.2, "Ordinateur\n\nMoniteur série", couleur=GRIS, taille=8.5)
esp = boite(ax, 8.0, 3.2, 4.0, 4.6, couleur=BLEU, fond="#f5f8fe")
ax.text(8.0, 5.1, "ESP32-S3", ha="center", fontsize=10, color=BLEU, fontweight="bold")
mod = boite(ax, 14.0, 3.0, 3.0, 3.8, couleur=ORANGE)
ax.text(14.0, 4.2, "Module externe\n(GPS, RFID…)", ha="center", va="center", fontsize=8.5, color=ENCRE)
# USB natif
boite(ax, 7.0, 4.2, 1.6, 0.6, "USB", couleur=GRIS, taille=8)
fleche(ax, (3.3, 4.2), (6.2, 4.2), "Serial\n(USB CDC)", double=True, couleur=VERT, taille=8)
# Broches Serial2
for yy, nom in ((3.0, "GPIO18 (TX)"), (2.0, "GPIO17 (RX)")):
ax.text(9.9, yy, nom, ha="right", va="center", fontsize=8.5, color=ENCRE)
for yy, nom in ((3.0, "RX"), (2.0, "TX")):
ax.text(12.7, yy, nom, ha="left", va="center", fontsize=8.5, color=ENCRE)
ax.text(9.9, 1.0, "GND", ha="right", va="center", fontsize=8.5, color=ENCRE)
ax.text(12.7, 1.0, "GND", ha="left", va="center", fontsize=8.5, color=ENCRE)
fleche(ax, (10.0, 3.0), (12.5, 2.0), couleur=BLEU)
fleche(ax, (12.5, 3.0), (10.0, 2.0), couleur=ORANGE)
ax.plot([10.0, 12.5], [1.0, 1.0], color=ENCRE, lw=1.3)
ax.text(11.25, 3.35, "croisé !", ha="center", fontsize=8.5, color=ORANGE, fontweight="bold")
ax.text(11.25, 0.55, "masse commune", ha="center", fontsize=7.5, color=GRIS)
ax.text(8.0, 0.15, "Serial2.begin(9600, SERIAL_8N1, 17, 18);", ha="center", fontsize=7.5,
color=GRIS, family="monospace")
return fig
FIGURES = [
Figure("m04-trame-uart", trame_uart),
Figure("m04-cablage-serie", cablage_serie),
]
+117
View File
@@ -0,0 +1,117 @@
"""Module 5 — Bus I2C et SPI."""
import matplotlib.pyplot as plt
from outils import boite, toile
from style import BLEU, CM, ENCRE, GRIS, ORANGE, VERT, Figure
def bus_i2c():
fig, ax = toile(16, 6.4)
maitre = boite(ax, 1.9, 3.2, 3.0, 2.6, couleur=BLEU)
ax.text(1.9, 3.75, "ESP32-S3", ha="center", fontsize=10, color=BLEU, fontweight="bold")
ax.text(1.9, 2.95, "maître\n(génère SCL)", ha="center", va="center", fontsize=8, color=ENCRE)
y_sda, y_scl = 4.0, 2.4
ax.plot([3.4, 15.6], [y_sda, y_sda], color=BLEU, lw=2)
ax.plot([3.4, 15.6], [y_scl, y_scl], color=ORANGE, lw=2)
ax.text(3.5, y_sda + 0.15, "SDA (GPIO8) — données", fontsize=8.5, color=BLEU, va="bottom")
ax.text(3.5, y_scl + 0.15, "SCL (GPIO9) — horloge", fontsize=8.5, color=ORANGE, va="bottom")
# Résistances de tirage
ax.plot([7.5, 9.5], [5.9, 5.9], color=ENCRE, lw=1.3)
ax.text(8.5, 6.05, "3V3", ha="center", fontsize=8, color=ENCRE)
for x, yb in ((7.8, y_sda), (9.2, y_scl)):
ax.plot([x, x], [5.9, 5.6], color=ENCRE, lw=1.3)
ax.add_patch(plt.Rectangle((x - 0.15, 4.7 if yb == y_sda else 4.2), 0.3, 0.9 if yb == y_sda else 1.4,
fill=False, ec=ENCRE, lw=1.3))
ax.plot([x, x], [4.7 if yb == y_sda else 4.2, yb], color=ENCRE, lw=1.3)
ax.plot(x, yb, "o", color=ENCRE, ms=4)
ax.text(9.5, 5.2, "résistances de tirage\n(souvent déjà sur les modules)", fontsize=7.5, color=GRIS, va="center")
# Esclaves
for x, nom, adr in ((6.0, "Écran OLED\nSSD1306", "0x3C"), (11.5, "Capteur\nBME280", "0x76"),
(14.6, "autre\nmodule", "0x??")):
boite(ax, x, 0.85, 2.7, 1.45, f"{nom}\nadresse {adr}", couleur=VERT, taille=7.2)
for dx, yb, c in ((-0.5, y_sda, BLEU), (0.5, y_scl, ORANGE)):
ax.plot([x + dx, x + dx], [1.5, yb], color=c, lw=1.5)
ax.plot(x + dx, yb, "o", color=c, ms=4)
ax.text(1.9, 1.05, "tous sur les 2 mêmes fils :\nchacun répond\nà son adresse", ha="center", va="center",
fontsize=7.2, color=GRIS, style="italic")
return fig
def trame_i2c():
# Écriture vers l'adresse 0x3C (0111100), R/W = 0, ACK, octet 0xAF, ACK
adresse = [0, 1, 1, 1, 1, 0, 0]
donnee = [1, 0, 1, 0, 1, 1, 1, 1]
bits = adresse + [0] + [0] + donnee + [0]
roles = ["adr"] * 7 + ["rw"] + ["ack"] + ["data"] * 8 + ["ack"]
fig, ax = plt.subplots(figsize=(16 * CM, 5.6 * CM))
t0 = 1.0
scl_x, scl_y, sda_x, sda_y = [0, t0], [1, 1], [0, 0.5], [1, 1]
sda_x += [0.5]
sda_y += [0] # START : SDA descend pendant que SCL est haut
scl_x += [t0]
scl_y += [0]
for k, b in enumerate(bits):
x = t0 + k
scl_x += [x + 0.5, x + 0.5, x + 1, x + 1]
scl_y += [0, 1, 1, 0]
sda_x += [x + 0.15, x + 0.15]
sda_y += [sda_y[-1], b]
fin = t0 + len(bits)
scl_x += [fin + 0.4, fin + 0.4, fin + 1.4]
scl_y += [0, 1, 1]
sda_x += [fin + 0.15, fin + 0.15, fin + 0.8, fin + 0.8, fin + 1.4]
sda_y += [sda_y[-1], 0, 0, 1, 1] # STOP : SDA monte pendant que SCL est haut
ax.plot(scl_x, [v + 2 for v in scl_y], color=ORANGE, lw=1.6)
ax.plot(sda_x, sda_y, color=BLEU, lw=1.6)
couleurs = {"adr": BLEU, "rw": BLEU, "ack": VERT, "data": BLEU}
for k, (b, r) in enumerate(zip(bits, roles)):
x = t0 + k
ax.axvspan(x, x + 1, ymin=0.08, ymax=0.42, color=couleurs[r], alpha=0.07 if r != "ack" else 0.2, lw=0)
ax.text(x + 0.5, -0.35, str(b), ha="center", fontsize=7.5, color=ENCRE)
groupes = [(0, 1, "START", ORANGE), (t0, t0 + 7, "adresse 0x3C (7 bits)", BLEU), (t0 + 7, t0 + 8, "R/W", BLEU),
(t0 + 8, t0 + 9, "ACK", VERT), (t0 + 9, t0 + 17, "donnée (8 bits)", BLEU),
(t0 + 17, t0 + 18, "ACK", VERT), (fin, fin + 1.4, "STOP", ORANGE)]
for a, b, txt, c in groupes:
ax.annotate("", xy=(a + 0.05, 3.45), xytext=(b - 0.05, 3.45),
arrowprops=dict(arrowstyle="|-|", color=c, lw=0.9, mutation_scale=3))
ax.text((a + b) / 2, 3.6, txt, ha="center", va="bottom", fontsize=7.3, color=c, fontweight="bold")
ax.text(-0.3, 2.5, "SCL", ha="right", va="center", fontsize=9, fontweight="bold", color=ORANGE)
ax.text(-0.3, 0.5, "SDA", ha="right", va="center", fontsize=9, fontweight="bold", color=BLEU)
ax.text(fin + 1.4, -0.75, "ACK (en vert) : c'est l'esclave qui tire SDA à 0 pour accuser réception",
ha="right", fontsize=7.5, color=VERT)
ax.set_xlim(-0.4, fin + 1.6)
ax.set_ylim(-0.95, 4.2)
ax.axis("off")
fig.tight_layout()
return fig
def bus_spi():
fig, ax = toile(16, 6.4)
boite(ax, 2.0, 3.2, 3.2, 4.4, couleur=BLEU)
ax.text(2.0, 4.55, "ESP32-S3\nmaître", ha="center", va="center", fontsize=9.5, color=BLEU, fontweight="bold")
lignes = [("SCK", 4.2, ORANGE, "horloge"), ("MOSI", 3.5, BLEU, "maître → esclave"),
("MISO", 2.8, VERT, "esclave → maître")]
for nom, yy, c, role in lignes:
ax.plot([3.6, 15.4], [yy, yy], color=c, lw=2)
ax.text(3.75, yy + 0.08, f"{nom} — {role}", fontsize=8, color=c, va="bottom")
esclaves = [(9.0, "Écran TFT", "CS1"), (13.5, "Carte SD", "CS2")]
for i, (x, nom, cs) in enumerate(esclaves):
boite(ax, x, 0.8, 2.8, 1.1, nom, couleur=GRIS, taille=8.5)
for dx, (_, yy, c, _r) in zip((-0.9, -0.3, 0.3), lignes):
ax.plot([x + dx, x + dx], [1.35, yy], color=c, lw=1.4)
ax.plot(x + dx, yy, "o", color=c, ms=3.5)
ycs = 1.9 - i * 0.45
ax.plot([3.6, x + 0.9, x + 0.9], [ycs, ycs, 1.35], color=ENCRE, lw=1.4, ls="--" if i else "-")
ax.text(3.75, ycs + 0.06, cs, fontsize=7.8,
color=ENCRE, va="bottom")
ax.text(15.4, 5.6, "SCK, MOSI, MISO : partagés\nCS (Chip Select) : une ligne par périphérique,\nle maître met à 0 celle de son interlocuteur",
ha="right", va="center", fontsize=7.8, color=GRIS, style="italic")
return fig
FIGURES = [
Figure("m05-bus-i2c", bus_i2c),
Figure("m05-trame-i2c", trame_i2c),
Figure("m05-bus-spi", bus_spi),
]
+122
View File
@@ -0,0 +1,122 @@
"""Module 6 — Interruptions matérielles & timers."""
import matplotlib.pyplot as plt
import numpy as np
from outils import chronogramme
from style import BLEU, CM, ENCRE, GRIS, ORANGE, ROUGE, VERT, Figure
def taches_millis():
fig, ax = plt.subplots(figsize=(16 * CM, 6.6 * CM))
duree = 5200
# loop() : itérations très rapides
ax.vlines(np.arange(0, duree, 40), 3.1, 3.7, color=GRIS, lw=0.6)
# LED toutes les 300 ms
t = np.arange(0, duree + 300, 300)
niveaux = np.arange(len(t)) % 2
ax.step(t, 2.0 + niveaux * 0.6, where="post", color=BLEU, lw=1.6)
# NeoPixel toutes les 1000 ms
teintes = [ROUGE, VERT, BLEU, "#f5f5f5", ROUGE, VERT]
for k in range(6):
ax.add_patch(plt.Rectangle((k * 1000, 0.95), min(1000, duree - k * 1000), 0.6,
facecolor=teintes[k], edgecolor=GRIS, lw=0.6, alpha=0.85))
# Debug toutes les 5000 ms
for x in (0, 5000):
ax.plot(x, 0.3, marker="v", color=ORANGE, ms=8)
ax.text(x + 60, 0.3, "Serial.print", fontsize=7, color=ORANGE, va="center")
noms = [(3.4, "loop()", GRIS), (2.3, "LED (300 ms)", BLEU), (1.25, "NeoPixel (1 s)", ENCRE),
(0.3, "debug (5 s)", ORANGE)]
for yy, n, c in noms:
ax.text(-80, yy, n, ha="right", va="center", fontsize=8.5, color=c, fontweight="bold")
ax.text(duree, 3.85, "loop() tourne en permanence : à chaque tour, on teste « l'heure est-elle venue ? »",
ha="right", fontsize=7.5, color=GRIS, style="italic")
ax.set_xlim(-50, duree)
ax.set_ylim(-0.1, 4.2)
ax.set_yticks([])
ax.spines["left"].set_visible(False)
ax.set_xticks(range(0, 5001, 1000), [f"{s} s" for s in range(6)])
ax.set_xlabel("millis()")
ax.grid(axis="y", visible=False)
fig.tight_layout()
return fig
def deroule_isr():
fig, ax = plt.subplots(figsize=(16 * CM, 5.0 * CM))
# Signal du bouton
ax.step([0, 3.0, 6.0, 10], [2.6, 1.9, 2.6, 2.6], where="post", color=BLEU, lw=1.6)
ax.text(-0.15, 2.25, "bouton\n(GPIO5)", ha="right", va="center", fontsize=8.5, fontweight="bold", color=BLEU)
ax.annotate("front descendant\n(FALLING)", xy=(3.0, 2.25), xytext=(1.2, 3.25), fontsize=7.5, color=BLEU,
arrowprops=dict(arrowstyle="-|>", color=BLEU, lw=0.8), ha="center")
# Exécution du processeur
y = 0.6
ax.broken_barh([(0, 3.0), (3.35, 6.65)], (y, 0.7), facecolors=BLEU, alpha=0.25, edgecolor=BLEU)
ax.broken_barh([(3.0, 0.35)], (y, 0.7), facecolors=ROUGE, edgecolor=ROUGE)
ax.broken_barh([(5.0, 1.2)], (y, 0.7), facecolors=VERT, alpha=0.55, edgecolor=VERT)
ax.text(-0.15, y + 0.35, "processeur", ha="right", va="center", fontsize=8.5, fontweight="bold", color=ENCRE)
ax.text(1.5, y + 0.35, "loop()", ha="center", va="center", fontsize=8)
ax.text(8.1, y + 0.35, "loop()", ha="center", va="center", fontsize=8)
ax.annotate("ISR très courte :\ninterruptionDetectee = true", xy=(3.17, y), xytext=(3.4, -0.55),
fontsize=7.5, color=ROUGE, arrowprops=dict(arrowstyle="-|>", color=ROUGE, lw=0.8))
ax.text(5.6, y + 0.35, "traitement", ha="center", va="center", fontsize=7.5)
ax.text(5.6, y + 0.95, "if (interruptionDetectee) …", ha="center", fontsize=7.2, color=VERT,
family="monospace")
ax.set_xlim(-1.6, 10)
ax.set_ylim(-0.8, 3.9)
ax.axis("off")
fig.tight_layout()
return fig
def modes_declenchement():
pts = [(0, 0), (1.5, 1), (4, 0), (6, 1), (8.5, 0)]
fig, ax, y = chronogramme([("signal", pts)], 10, largeur_cm=16, hauteur_voie_cm=3.4)
ax.set_xticks([])
ax.spines["bottom"].set_visible(False)
lignes = [("RISING", [1.5, 6], BLEU), ("FALLING", [4, 8.5], ORANGE), ("CHANGE", [1.5, 4, 6, 8.5], VERT)]
for k, (nom, xs, c) in enumerate(lignes):
yy = -0.7 - k * 0.7
ax.text(-0.25, yy, nom, ha="right", va="center", fontsize=8.5, fontweight="bold", color=c)
ax.plot([0, 10], [yy, yy], color="#e5e7eb", lw=0.8)
for x in xs:
ax.plot(x, yy, marker="o", color=c, ms=7)
ax.plot([x, x], [yy, 0], color=c, lw=0.6, ls=":")
ax.set_ylim(-3.0, 1.5)
ax.text(10, -2.8, "● = instant où l'ISR est appelée", ha="right", fontsize=7.5, color=GRIS)
fig.tight_layout()
return fig
def encodeur():
fig, axes = plt.subplots(1, 2, figsize=(16 * CM, 5.2 * CM))
cas = [("Rotation dans un sens", 0.0, "DT = HIGH → compteur++", BLEU),
("Rotation dans l'autre sens", 0.5, "DT = LOW → compteur--", ORANGE)]
t = np.linspace(0, 4, 2000)
for ax, (titre, dephasage, regle, c) in zip(axes, cas):
clk = ((t % 2) < 1).astype(float)
dt = (((t - 0.5 + dephasage * 2) % 2) < 1).astype(float)
ax.plot(t, clk + 1.6, color=BLEU, lw=1.6)
ax.plot(t, dt, color=GRIS, lw=1.6)
ax.text(-0.1, 2.1, "CLK", ha="right", va="center", fontsize=8.5, fontweight="bold", color=BLEU)
ax.text(-0.1, 0.5, "DT", ha="right", va="center", fontsize=8.5, fontweight="bold", color=GRIS)
for x in (1, 3):
ax.plot([x, x], [-0.25, 2.8], color=c, lw=1, ls="--")
etat = dt[np.searchsorted(t, x + 0.01)]
ax.plot(x, etat, "o", color=c, ms=6)
ax.set_title(titre, fontsize=9, loc="left")
ax.text(2, -0.75, regle, ha="center", fontsize=8.5, color=c, fontweight="bold")
ax.set_xlim(-0.6, 4)
ax.set_ylim(-1.0, 2.9)
ax.axis("off")
fig.text(0.5, 0.02, "À chaque front descendant de CLK (pointillés), l'ISR lit l'état de DT. "
"Le sens réel dépend du câblage.", ha="center", fontsize=7.5, color=GRIS, style="italic")
fig.tight_layout(rect=(0, 0.06, 1, 1))
return fig
FIGURES = [
Figure("m06-taches-millis", taches_millis),
Figure("m06-deroule-isr", deroule_isr),
Figure("m06-modes-declenchement", modes_declenchement),
Figure("m06-encodeur", encodeur),
]
+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),
]
+61
View File
@@ -0,0 +1,61 @@
"""Module 10 — Gestion de l'énergie."""
import matplotlib.pyplot as plt
import numpy as np
from style import BLEU, CM, ENCRE, GRIS, ORANGE, ROUGE, VERT, Figure
def consommation_modes():
fig, ax = plt.subplots(figsize=(16 * CM, 6.4 * CM))
modes = [("Actif\n(Wi-Fi, calcul)", 80, 250, ROUGE, "moins d'1 jour"),
("Modem-sleep", 20, 30, ORANGE, "≈ 3 jours"),
("Light sleep", 0.7, 0.9, BLEU, "≈ 3 mois"),
("Deep sleep", 0.010, 0.150, VERT, "plus d'1 an (théorique)")]
for i, (nom, bas, haut, c, autonomie) in enumerate(modes):
ax.barh(i, haut - bas, left=bas, height=0.5, color=c, alpha=0.85)
ax.text(haut * 1.4, i, autonomie, va="center", fontsize=8.5, color=c, fontweight="bold")
ax.set_yticks(range(len(modes)), [m[0] for m in modes])
ax.invert_yaxis()
ax.set_xscale("log")
ax.set_xlim(0.005, 20000)
ax.set_xticks([0.01, 0.1, 1, 10, 100, 1000], ["10 µA", "100 µA", "1 mA", "10 mA", "100 mA", "1 A"])
ax.set_xlabel("courant consommé (échelle logarithmique)")
ax.text(20000, -0.75, "autonomie avec une batterie de 2000 mAh", ha="right", fontsize=7.5,
color=GRIS, style="italic")
ax.grid(axis="y", visible=False)
fig.tight_layout()
return fig
def cycle_deep_sleep():
fig, ax = plt.subplots(figsize=(16 * CM, 6.0 * CM))
t, i = [0], [0.05]
for debut in (0, 60, 120):
t += [debut, debut + 0.4, debut + 1.0, debut + 3.0]
i += [45, 40, 160, 0.05]
t += [150]
i += [0.05]
ax.step(t, i, where="post", color=BLEU, lw=1.6)
ax.fill_between(t, 0.01, i, step="post", color=BLEU, alpha=0.12)
ax.set_yscale("log")
ax.set_ylim(0.01, 600)
ax.set_yticks([0.01, 0.1, 1, 10, 100], ["10 µA", "100 µA", "1 mA", "10 mA", "100 mA"])
ax.set_xlim(-3, 150)
ax.set_xlabel("temps (s)")
ax.set_ylabel("courant")
ax.annotate("réveil : setup() repart du début\nmesure BME280, envoi Wi-Fi (≈ 3 s)", xy=(61.5, 160), xytext=(66, 260),
fontsize=7.8, color=ROUGE, arrowprops=dict(arrowstyle="-|>", color=ROUGE, lw=0.8))
ax.text(30, 0.5, "deep sleep (≈ 57 s)\nseule la RTC reste alimentée", ha="center", fontsize=7.8, color=VERT)
ax.text(31.5, 0.16, "esp_sleep_enable_timer_wakeup(60 s)", ha="center", fontsize=7.3, color=ENCRE,
family="monospace")
ax.text(91, 2.0, "compteurReveils (RTC_DATA_ATTR)\nconservé d'un cycle à l'autre", ha="center", fontsize=7.5,
color=GRIS, style="italic")
ax.grid(axis="x", visible=False)
fig.tight_layout()
return fig
FIGURES = [
Figure("m10-consommation-modes", consommation_modes),
Figure("m10-cycle-deep-sleep", cycle_deep_sleep),
]
+64
View File
@@ -0,0 +1,64 @@
"""Module 11 — IoT & Cloud : le protocole MQTT."""
from outils import boite, fleche, toile
from style import BLEU, ENCRE, GRIS, ORANGE, ROUGE, VERT, Figure
def publish_subscribe():
fig, ax = toile(16, 6.6)
esp = boite(ax, 2.2, 3.3, 3.4, 2.4, couleur=BLEU)
ax.text(2.2, 4.05, "ESP32-S3", ha="center", fontsize=10, fontweight="bold", color=BLEU)
ax.text(2.2, 3.0, "BME280 + LED\nNeoPixel", ha="center", va="center", fontsize=8, color=ENCRE)
broker = boite(ax, 8.0, 3.3, 3.0, 2.4, couleur=ORANGE)
ax.text(8.0, 3.75, "Broker", ha="center", fontsize=10, fontweight="bold", color=ORANGE)
ax.text(8.0, 3.0, "Mosquitto,\nHiveMQ…", ha="center", va="center", fontsize=8, color=ENCRE)
dash = boite(ax, 13.8, 4.8, 3.6, 1.4, "Tableau de bord\n(Node-RED)", couleur=VERT, taille=8.5)
ha = boite(ax, 13.8, 1.8, 3.6, 1.4, "Home Assistant", couleur=VERT, taille=8.5)
# Publication de la température
fleche(ax, (3.9, 3.9), (6.5, 3.9), "publish", couleur=BLEU, taille=8, fond=True)
ax.text(5.3, 5.1, "formation/esp32s3/temperature\n« 21.5 »", ha="center", fontsize=6.8, color=BLEU,
family="monospace")
fleche(ax, (9.5, 3.9), (12.0, 4.6), couleur=BLEU)
fleche(ax, (9.5, 3.7), (12.0, 2.0), couleur=BLEU)
ax.text(12.4, 3.35, "abonnés à …/temperature\n→ reçoivent la valeur", ha="center", fontsize=7.2,
color=GRIS, style="italic")
# Commande en retour
fleche(ax, (12.0, 5.2), (9.5, 4.3), couleur=ROUGE)
fleche(ax, (6.5, 2.7), (3.9, 2.7), "message", couleur=ROUGE, taille=8, fond=True)
ax.text(5.3, 1.45, "formation/esp32s3/commande\n« rouge »", ha="center", fontsize=6.8, color=ROUGE,
family="monospace")
ax.text(10.9, 5.25, "publish « rouge »", ha="center", fontsize=7.5, color=ROUGE, rotation=20)
ax.text(8.0, 0.35, "Les appareils ne se parlent jamais directement : tout passe par le broker.",
ha="center", fontsize=7.8, color=GRIS, style="italic")
return fig
def arborescence_topics():
fig, ax = toile(16, 5.0)
noeuds = {
"maison": (1.6, 3.6), "salon": (4.9, 4.3), "cuisine": (4.9, 2.9),
"temperature ": (8.3, 4.65), "humidite": (8.3, 3.95), "temperature ": (8.3, 2.9),
"formation": (1.6, 1.2), "esp32s3": (4.9, 1.2),
"temperature": (8.3, 1.55), "commande": (8.3, 0.85),
}
liens = [("maison", "salon"), ("maison", "cuisine"), ("salon", "temperature "), ("salon", "humidite"),
("cuisine", "temperature "), ("formation", "esp32s3"), ("esp32s3", "temperature"),
("esp32s3", "commande")]
for a, b in liens:
(x1, y1), (x2, y2) = noeuds[a], noeuds[b]
ax.plot([x1 + 1.15, (x1 + x2) / 2 + 0.3, (x1 + x2) / 2 + 0.3, x2 - 1.15], [y1, y1, y2, y2],
color=GRIS, lw=1.1)
for nom, (x, y) in noeuds.items():
c = BLEU if nom.startswith(("formation", "esp32s3")) or nom in ("temperature", "commande") else GRIS
boite(ax, x, y, 2.3, 0.5, nom.strip(), couleur=c, taille=7.2)
chemins = [(4.65, "maison/salon/temperature", GRIS), (2.9, "maison/cuisine/temperature", GRIS),
(1.55, "formation/esp32s3/temperature", BLEU), (0.85, "formation/esp32s3/commande", BLEU)]
for y, txt, c in chemins:
ax.text(9.75, y, "→ " + txt, va="center", fontsize=7.8, color=c, family="monospace")
ax.text(9.75, 3.95, "→ maison/salon/humidite", va="center", fontsize=7.8, color=GRIS, family="monospace")
return fig
FIGURES = [
Figure("m11-publish-subscribe", publish_subscribe),
Figure("m11-arborescence-topics", arborescence_topics),
]
+43
View File
@@ -0,0 +1,43 @@
"""Module 12 — Projet final."""
from outils import boite, fleche, toile
from style import BLEU, ENCRE, GRIS, ORANGE, VERT, Figure
def demarche_projet():
fig, ax = toile(16, 4.2)
etapes = [("Besoin", "quel\nproblème ?", GRIS), ("Cahier des\ncharges", "matériel,\nmodules", BLEU),
("MVP", "l'indispensable\nd'abord", BLEU), ("Tests", "sur une durée\nréaliste", ORANGE),
("Bonus", "si le temps\nle permet", VERT), ("Présentation", "démo, choix,\ndifficultés", VERT)]
xs = [1.3 + k * 2.68 for k in range(len(etapes))]
for x, (nom, sous, c) in zip(xs, etapes):
boite(ax, x, 2.6, 2.35, 1.1, nom, couleur=c, taille=7.2, gras=True)
ax.text(x, 1.85, sous, ha="center", va="top", fontsize=7, color=GRIS)
for a, b in zip(xs, xs[1:]):
fleche(ax, (a + 1.175, 2.6), (b - 1.175, 2.6))
fleche(ax, (xs[3], 3.15), (xs[2], 3.15), "corriger", couleur=ORANGE, taille=7.3, courbe=0.6)
return fig
def structure_projet():
fig, ax = toile(16, 6.0)
main = boite(ax, 8.0, 4.6, 6.2, 1.6, couleur=BLEU)
ax.text(8.0, 5.05, "MonProjet.ino", ha="center", fontsize=9.5, fontweight="bold", color=BLEU,
family="monospace")
ax.text(8.0, 4.3, "setup() et loop() uniquement :\norchestrent les appels", ha="center", va="center",
fontsize=7.8, color=ENCRE)
onglets = [(2.8, "capteurs.ino", "lireTemperature()\nlireHumidite()", VERT),
(8.0, "affichage.ino", "afficherEcran()\nafficherLed()", ORANGE),
(13.2, "reseau.ino", "connecterWiFi()\npublierMQTT()", GRIS)]
for x, nom, fonctions, c in onglets:
boite(ax, x, 1.4, 4.2, 1.9, couleur=c)
ax.text(x, 1.95, nom, ha="center", fontsize=9, fontweight="bold", color=c, family="monospace")
ax.text(x, 1.1, fonctions, ha="center", va="center", fontsize=7.8, color=ENCRE, family="monospace")
fleche(ax, (8.0 + (x - 8.0) * 0.25, main["b"][1]), (x, 2.35), couleur=c)
ax.text(15.8, 5.6, "une fonction =\nune responsabilité", ha="right", fontsize=7.8, color=GRIS, style="italic")
return fig
FIGURES = [
Figure("m12-demarche-projet", demarche_projet),
Figure("m12-structure-projet", structure_projet),
]
+38
View File
@@ -0,0 +1,38 @@
"""Synthèse de la formation (préfixe m13 : document hors numérotation des modules)."""
from outils import boite, fleche, toile
from style import BLEU, GRIS, ORANGE, ROUGE, VERT, Figure
def parcours_formation():
fig, ax = toile(16, 8.8)
colonnes = [
(1.55, "Prise\nen main", GRIS, [("0", "Installation"), ("1", "Microcontrôleur")]),
(4.775, "Entrées /\nsorties", BLEU, [("2", "GPIO & NeoPixel"), ("3", "PWM & ADC"), ("4", "Série & debug")]),
(8.0, "Bus\n& temps", ORANGE, [("5", "I2C & SPI"), ("6", "Interruptions")]),
(11.225, "Connectivité", VERT, [("7", "Wi-Fi"), ("8", "Serveur web"), ("9", "BLE"), ("11", "MQTT")]),
(14.45, "Autonomie", ROUGE, [("10", "Énergie")]),
]
centres = {}
for x, titre, c, modules in colonnes:
ax.text(x, 8.2, titre, ha="center", va="center", fontsize=8.5, fontweight="bold", color=c)
n = len(modules)
for k, (num, nom) in enumerate(modules):
y = 6.95 - k * 1.25 - (4 - n) * 0.3
boite(ax, x, y, 2.75, 1.0, f"M{num}\n{nom}", couleur=c, taille=7.1)
centres[num] = (x, y)
for a, b in (("1", "2"), ("4", "5"), ("6", "7"), ("11", "10")):
(x1, y1), (x2, y2) = centres[a], centres[b]
fleche(ax, (x1 + 1.375, y1), (x2 - 1.375, y2), couleur=GRIS)
projet = boite(ax, 8.0, 0.8, 9.6, 1.1, "M12 — Projet final : au moins 3 modules combinés", couleur=BLEU,
taille=9, gras=True)
for x, *_ in colonnes:
cible = (min(max(x, 3.6), 12.4), projet["h"][1])
fleche(ax, (x, 2.4), cible, couleur=GRIS, pointille=True)
ax.text(14.45, 4.5, "la LED NeoPixel\nsert de fil rouge\ndans tous\nles modules", ha="center", va="center",
fontsize=7.3, color=GRIS, style="italic")
return fig
FIGURES = [
Figure("m13-parcours-formation", parcours_formation),
]
+152
View File
@@ -0,0 +1,152 @@
"""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 = plt.figure(figsize=(largeur_cm * CM, hauteur_cm * CM))
ax = fig.add_axes((0, 0, 1, 1)) # pas de marges : 1 unité = 1 cm, police et boîtes à la même échelle
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:
t = ax.text(x, y, texte, ha="center", va="center", fontsize=taille, color=ENCRE,
fontweight="bold" if gras else "normal", linespacing=1.3)
ajuster_texte(ax, t, l, h)
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 ajuster_texte(ax, t, l, h, marge=0.86, taille_min=5.5):
"""Réduit la police du texte t jusqu'à ce qu'il tienne dans une boîte l × h (unités de données)."""
rendu = ax.figure.canvas.get_renderer()
ax.apply_aspect() # sinon la mise à l'échelle « equal » n'est appliquée qu'au rendu final
(x0, y0), (x1, y1) = ax.transData.transform([(0, 0), (l, h)])
largeur_max, hauteur_max = abs(x1 - x0) * marge, abs(y1 - y0) * marge
while t.get_fontsize() > taille_min:
ext = t.get_window_extent(rendu)
if ext.width <= largeur_max and ext.height <= hauteur_max:
break
t.set_fontsize(t.get_fontsize() * 0.95)
def fleche(ax, depart, arrivee, texte="", couleur=ENCRE, taille=8, double=False, pointille=False,
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,
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, bbox=cadre)
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)
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:
# 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,
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")
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"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+6 -4
View File
@@ -35,6 +35,7 @@ plt.rcParams.update({
"ytick.color": GRIS,
"lines.linewidth": 2,
"svg.fonttype": "path", # texte vectorisé : rendu identique partout
"svg.hashsalt": "formation-esp32", # identifiants SVG stables d'une génération à l'autre
"savefig.bbox": "tight",
"savefig.pad_inches": 0.1,
"savefig.facecolor": "white",
@@ -43,9 +44,10 @@ plt.rcParams.update({
@dataclass
class Figure:
"""Déclaration d'une figure à générer puis insérer dans un support."""
"""Déclaration d'une figure à générer.
La légende et l'emplacement sont dans le .qmd :
![Légende](../schemas/svg/<id>.svg){#fig-<id>}
"""
id: str # ex. "m03-pwm-duty" ; le préfixe mXX donne le module
legende: str # texte de la légende (sans « Figure X.Y — »)
ancre: str # texte exact du titre de section concerné
dessiner: Callable # renvoie une figure matplotlib ou un dessin schemdraw
largeur_cm: float = 16.0
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 62 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: 28 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 53 KiB

+940
View File
@@ -0,0 +1,940 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="484.918799pt" height="325.741571pt" viewBox="0 0 484.918799 325.741571" xmlns="http://www.w3.org/2000/svg" version="1.1">
<metadata>
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<cc:Work>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:format>image/svg+xml</dc:format>
<dc:creator>
<cc:Agent>
<dc:title>Matplotlib v3.11.2, https://matplotlib.org/</dc:title>
</cc:Agent>
</dc:creator>
</cc:Work>
</rdf:RDF>
</metadata>
<defs>
<style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}</style>
</defs>
<g id="figure_1">
<g id="patch_1">
<path d="M 0 325.741571
L 484.918799 325.741571
L 484.918799 0
L 0 0
z
" style="fill: #ffffff"/>
</g>
<g id="axes_1">
<g id="patch_2">
<path d="M 353.84179 46.025854
L 369.194479 56.992061
L 353.84179 67.958267
L 353.84179 46.025854
z
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #c0392b; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_3">
<path d="M 353.84179 110.360933
L 369.194479 121.327139
L 353.84179 132.293346
L 353.84179 110.360933
z
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_4">
<path d="M 353.84179 174.696011
L 369.194479 185.662218
L 353.84179 196.628425
L 353.84179 174.696011
z
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_5">
<path d="M 353.84179 239.03109
L 369.194479 249.997297
L 353.84179 260.963503
L 353.84179 239.03109
z
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_1">
<path d="M 28.145455 271.92971
L 168.512899 271.92971
L 168.512899 35.059648
L 28.145455 35.059648
L 28.145455 271.92971
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_2">
<path d="M 168.512899 249.997297
L 190.445312 249.997297
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_3">
<path d="M 168.512899 185.662218
L 190.445312 185.662218
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_4">
<path d="M 168.512899 121.327139
L 190.445312 121.327139
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_5">
<path d="M 168.512899 56.992061
L 190.445312 56.992061
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_6">
<path d="M 190.445312 56.992061
L 203.60476 56.992061
L 216.764208 56.992061
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_7">
<path d="M 216.764208 56.992061
L 243.083103 56.992061
L 246.738505 46.025854
L 254.04931 67.958267
L 261.360114 46.025854
L 268.670919 67.958267
L 275.981723 46.025854
L 283.292527 67.958267
L 286.94793 56.992061
L 313.266825 56.992061
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_8">
<path d="M 313.266825 56.992061
L 353.84179 56.992061
M 369.194479 46.025854
L 369.194479 67.958267
M 369.194479 56.992061
L 409.769443 56.992061
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #c0392b; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_9">
<path d="M 364.807996 40.542751
L 371.421593 28.968956
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #c0392b; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_6">
<path d="M 368.374766 27.227913
L 375.774203 21.35189
L 374.46842 30.71
z
" clip-path="url(#p7e999a346a)" style="fill: #c0392b"/>
</g>
<g id="line2d_10">
<path d="M 354.93841 40.542751
L 361.552007 28.968956
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #c0392b; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_7">
<path d="M 358.50518 27.227913
L 365.904617 21.35189
L 364.598834 30.71
z
" clip-path="url(#p7e999a346a)" style="fill: #c0392b"/>
</g>
<g id="line2d_11">
<path d="M 409.769443 56.992061
L 422.928891 56.992061
L 436.088339 56.992061
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_12">
<path d="M 190.445312 121.327139
L 203.60476 121.327139
L 216.764208 121.327139
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_13">
<path d="M 216.764208 121.327139
L 243.083103 121.327139
L 246.738505 110.360933
L 254.04931 132.293346
L 261.360114 110.360933
L 268.670919 132.293346
L 275.981723 110.360933
L 283.292527 132.293346
L 286.94793 121.327139
L 313.266825 121.327139
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_14">
<path d="M 313.266825 121.327139
L 353.84179 121.327139
M 369.194479 110.360933
L 369.194479 132.293346
M 369.194479 121.327139
L 409.769443 121.327139
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_15">
<path d="M 364.807996 104.87783
L 371.421593 93.304035
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_8">
<path d="M 368.374766 91.562991
L 375.774203 85.686968
L 374.46842 95.045079
z
" clip-path="url(#p7e999a346a)" style="fill: #2e8b57"/>
</g>
<g id="line2d_16">
<path d="M 354.93841 104.87783
L 361.552007 93.304035
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_9">
<path d="M 358.50518 91.562991
L 365.904617 85.686968
L 364.598834 95.045079
z
" clip-path="url(#p7e999a346a)" style="fill: #2e8b57"/>
</g>
<g id="line2d_17">
<path d="M 409.769443 121.327139
L 422.928891 121.327139
L 436.088339 121.327139
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_18">
<path d="M 190.445312 185.662218
L 203.60476 185.662218
L 216.764208 185.662218
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_19">
<path d="M 216.764208 185.662218
L 243.083103 185.662218
L 246.738505 174.696011
L 254.04931 196.628425
L 261.360114 174.696011
L 268.670919 196.628425
L 275.981723 174.696011
L 283.292527 196.628425
L 286.94793 185.662218
L 313.266825 185.662218
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_20">
<path d="M 313.266825 185.662218
L 353.84179 185.662218
M 369.194479 174.696011
L 369.194479 196.628425
M 369.194479 185.662218
L 409.769443 185.662218
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_21">
<path d="M 364.807996 169.212908
L 371.421593 157.639114
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_10">
<path d="M 368.374766 155.89807
L 375.774203 150.022047
L 374.46842 159.380157
z
" clip-path="url(#p7e999a346a)" style="fill: #1f5fa8"/>
</g>
<g id="line2d_22">
<path d="M 354.93841 169.212908
L 361.552007 157.639114
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_11">
<path d="M 358.50518 155.89807
L 365.904617 150.022047
L 364.598834 159.380157
z
" clip-path="url(#p7e999a346a)" style="fill: #1f5fa8"/>
</g>
<g id="line2d_23">
<path d="M 409.769443 185.662218
L 422.928891 185.662218
L 436.088339 185.662218
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_24">
<path d="M 190.445312 249.997297
L 203.60476 249.997297
L 216.764208 249.997297
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_25">
<path d="M 216.764208 249.997297
L 243.083103 249.997297
L 246.738505 239.03109
L 254.04931 260.963503
L 261.360114 239.03109
L 268.670919 260.963503
L 275.981723 239.03109
L 283.292527 260.963503
L 286.94793 249.997297
L 313.266825 249.997297
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_26">
<path d="M 313.266825 249.997297
L 353.84179 249.997297
M 369.194479 239.03109
L 369.194479 260.963503
M 369.194479 249.997297
L 409.769443 249.997297
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_27">
<path d="M 364.807996 233.547987
L 371.421593 221.974192
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_12">
<path d="M 368.374766 220.233148
L 375.774203 214.357125
L 374.46842 223.715236
z
" clip-path="url(#p7e999a346a)" style="fill: #e07a1f"/>
</g>
<g id="line2d_28">
<path d="M 354.93841 233.547987
L 361.552007 221.974192
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_13">
<path d="M 358.50518 220.233148
L 365.904617 214.357125
L 364.598834 223.715236
z
" clip-path="url(#p7e999a346a)" style="fill: #e07a1f"/>
</g>
<g id="line2d_29">
<path d="M 409.769443 249.997297
L 422.928891 249.997297
L 436.088339 249.997297
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_30">
<path d="M 436.088339 56.992061
L 436.088339 153.494679
L 436.088339 249.997297
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_31">
<path d="M 436.088339 249.997297
L 436.088339 263.156744
L 436.088339 276.316192
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_32">
<path d="M 436.088339 276.316192
L 436.088339 293.862123
L 425.122132 293.862123
L 447.054545 293.862123
M 428.411994 299.125902
L 443.764683 299.125902
M 433.895098 304.389681
L 438.28158 304.389681
" clip-path="url(#p7e999a346a)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="text_1">
<!-- GPIO7 -->
<g style="fill: #1f5fa8" transform="translate(118.585675 255.093078) scale(0.14 -0.14)">
<defs>
<path id="DejaVuSans-2a" d="M 3809 666
L 3809 1919
L 2778 1919
L 2778 2438
L 4434 2438
L 4434 434
Q 4069 175 3628 42
Q 3188 -91 2688 -91
Q 1594 -91 976 548
Q 359 1188 359 2328
Q 359 3472 976 4111
Q 1594 4750 2688 4750
Q 3144 4750 3555 4637
Q 3966 4525 4313 4306
L 4313 3634
Q 3963 3931 3569 4081
Q 3175 4231 2741 4231
Q 1884 4231 1454 3753
Q 1025 3275 1025 2328
Q 1025 1384 1454 906
Q 1884 428 2741 428
Q 3075 428 3337 486
Q 3600 544 3809 666
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-33" d="M 1259 4147
L 1259 2394
L 2053 2394
Q 2494 2394 2734 2622
Q 2975 2850 2975 3272
Q 2975 3691 2734 3919
Q 2494 4147 2053 4147
L 1259 4147
z
M 628 4666
L 2053 4666
Q 2838 4666 3239 4311
Q 3641 3956 3641 3272
Q 3641 2581 3239 2228
Q 2838 1875 2053 1875
L 1259 1875
L 1259 0
L 628 0
L 628 4666
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-2c" d="M 628 4666
L 1259 4666
L 1259 0
L 628 0
L 628 4666
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-32" d="M 2522 4238
Q 1834 4238 1429 3725
Q 1025 3213 1025 2328
Q 1025 1447 1429 934
Q 1834 422 2522 422
Q 3209 422 3611 934
Q 4013 1447 4013 2328
Q 4013 3213 3611 3725
Q 3209 4238 2522 4238
z
M 2522 4750
Q 3503 4750 4090 4092
Q 4678 3434 4678 2328
Q 4678 1225 4090 567
Q 3503 -91 2522 -91
Q 1538 -91 948 565
Q 359 1222 359 2328
Q 359 3434 948 4092
Q 1538 4750 2522 4750
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-1a" d="M 525 4666
L 3525 4666
L 3525 4397
L 1831 0
L 1172 0
L 2766 4134
L 525 4134
L 525 4666
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-2a"/>
<use xlink:href="#DejaVuSans-33" transform="translate(77.484375 0)"/>
<use xlink:href="#DejaVuSans-2c" transform="translate(137.78125 0)"/>
<use xlink:href="#DejaVuSans-32" transform="translate(167.28125 0)"/>
<use xlink:href="#DejaVuSans-1a" transform="translate(246 0)"/>
</g>
</g>
<g id="text_2">
<!-- GPIO6 -->
<g style="fill: #1f5fa8" transform="translate(118.585675 190.757999) scale(0.14 -0.14)">
<defs>
<path id="DejaVuSans-19" d="M 2113 2584
Q 1688 2584 1439 2293
Q 1191 2003 1191 1497
Q 1191 994 1439 701
Q 1688 409 2113 409
Q 2538 409 2786 701
Q 3034 994 3034 1497
Q 3034 2003 2786 2293
Q 2538 2584 2113 2584
z
M 3366 4563
L 3366 3988
Q 3128 4100 2886 4159
Q 2644 4219 2406 4219
Q 1781 4219 1451 3797
Q 1122 3375 1075 2522
Q 1259 2794 1537 2939
Q 1816 3084 2150 3084
Q 2853 3084 3261 2657
Q 3669 2231 3669 1497
Q 3669 778 3244 343
Q 2819 -91 2113 -91
Q 1303 -91 875 529
Q 447 1150 447 2328
Q 447 3434 972 4092
Q 1497 4750 2381 4750
Q 2619 4750 2861 4703
Q 3103 4656 3366 4563
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-2a"/>
<use xlink:href="#DejaVuSans-33" transform="translate(77.484375 0)"/>
<use xlink:href="#DejaVuSans-2c" transform="translate(137.78125 0)"/>
<use xlink:href="#DejaVuSans-32" transform="translate(167.28125 0)"/>
<use xlink:href="#DejaVuSans-19" transform="translate(246 0)"/>
</g>
</g>
<g id="text_3">
<!-- GPIO5 -->
<g style="fill: #1f5fa8" transform="translate(118.585675 126.422921) scale(0.14 -0.14)">
<defs>
<path id="DejaVuSans-18" d="M 691 4666
L 3169 4666
L 3169 4134
L 1269 4134
L 1269 2991
Q 1406 3038 1543 3061
Q 1681 3084 1819 3084
Q 2600 3084 3056 2656
Q 3513 2228 3513 1497
Q 3513 744 3044 326
Q 2575 -91 1722 -91
Q 1428 -91 1123 -41
Q 819 9 494 109
L 494 744
Q 775 591 1075 516
Q 1375 441 1709 441
Q 2250 441 2565 725
Q 2881 1009 2881 1497
Q 2881 1984 2565 2268
Q 2250 2553 1709 2553
Q 1456 2553 1204 2497
Q 953 2441 691 2322
L 691 4666
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-2a"/>
<use xlink:href="#DejaVuSans-33" transform="translate(77.484375 0)"/>
<use xlink:href="#DejaVuSans-2c" transform="translate(137.78125 0)"/>
<use xlink:href="#DejaVuSans-32" transform="translate(167.28125 0)"/>
<use xlink:href="#DejaVuSans-18" transform="translate(246 0)"/>
</g>
</g>
<g id="text_4">
<!-- GPIO4 -->
<g style="fill: #1f5fa8" transform="translate(118.585675 62.087842) scale(0.14 -0.14)">
<defs>
<path id="DejaVuSans-17" d="M 2419 4116
L 825 1625
L 2419 1625
L 2419 4116
z
M 2253 4666
L 3047 4666
L 3047 1625
L 3713 1625
L 3713 1100
L 3047 1100
L 3047 0
L 2419 0
L 2419 1100
L 313 1100
L 313 1709
L 2253 4666
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-2a"/>
<use xlink:href="#DejaVuSans-33" transform="translate(77.484375 0)"/>
<use xlink:href="#DejaVuSans-2c" transform="translate(137.78125 0)"/>
<use xlink:href="#DejaVuSans-32" transform="translate(167.28125 0)"/>
<use xlink:href="#DejaVuSans-17" transform="translate(246 0)"/>
</g>
</g>
<g id="text_5">
<!-- ESP32-S3 -->
<g style="fill: #1f5fa8" transform="translate(74.071284 153.476009) scale(0.12 -0.12)">
<defs>
<path id="DejaVuSans-28" d="M 628 4666
L 3578 4666
L 3578 4134
L 1259 4134
L 1259 2753
L 3481 2753
L 3481 2222
L 1259 2222
L 1259 531
L 3634 531
L 3634 0
L 628 0
L 628 4666
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-36" d="M 3425 4513
L 3425 3897
Q 3066 4069 2747 4153
Q 2428 4238 2131 4238
Q 1616 4238 1336 4038
Q 1056 3838 1056 3469
Q 1056 3159 1242 3001
Q 1428 2844 1947 2747
L 2328 2669
Q 3034 2534 3370 2195
Q 3706 1856 3706 1288
Q 3706 609 3251 259
Q 2797 -91 1919 -91
Q 1588 -91 1214 -16
Q 841 59 441 206
L 441 856
Q 825 641 1194 531
Q 1563 422 1919 422
Q 2459 422 2753 634
Q 3047 847 3047 1241
Q 3047 1584 2836 1778
Q 2625 1972 2144 2069
L 1759 2144
Q 1053 2284 737 2584
Q 422 2884 422 3419
Q 422 4038 858 4394
Q 1294 4750 2059 4750
Q 2388 4750 2728 4690
Q 3069 4631 3425 4513
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-16" d="M 2597 2516
Q 3050 2419 3304 2112
Q 3559 1806 3559 1356
Q 3559 666 3084 287
Q 2609 -91 1734 -91
Q 1441 -91 1130 -33
Q 819 25 488 141
L 488 750
Q 750 597 1062 519
Q 1375 441 1716 441
Q 2309 441 2620 675
Q 2931 909 2931 1356
Q 2931 1769 2642 2001
Q 2353 2234 1838 2234
L 1294 2234
L 1294 2753
L 1863 2753
Q 2328 2753 2575 2939
Q 2822 3125 2822 3475
Q 2822 3834 2567 4026
Q 2313 4219 1838 4219
Q 1578 4219 1281 4162
Q 984 4106 628 3988
L 628 4550
Q 988 4650 1302 4700
Q 1616 4750 1894 4750
Q 2613 4750 3031 4423
Q 3450 4097 3450 3541
Q 3450 3153 3228 2886
Q 3006 2619 2597 2516
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-15" d="M 1228 531
L 3431 531
L 3431 0
L 469 0
L 469 531
Q 828 903 1448 1529
Q 2069 2156 2228 2338
Q 2531 2678 2651 2914
Q 2772 3150 2772 3378
Q 2772 3750 2511 3984
Q 2250 4219 1831 4219
Q 1534 4219 1204 4116
Q 875 4013 500 3803
L 500 4441
Q 881 4594 1212 4672
Q 1544 4750 1819 4750
Q 2544 4750 2975 4387
Q 3406 4025 3406 3419
Q 3406 3131 3298 2873
Q 3191 2616 2906 2266
Q 2828 2175 2409 1742
Q 1991 1309 1228 531
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-10" d="M 313 2009
L 1997 2009
L 1997 1497
L 313 1497
L 313 2009
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-28"/>
<use xlink:href="#DejaVuSans-36" transform="translate(63.1875 0)"/>
<use xlink:href="#DejaVuSans-33" transform="translate(126.671875 0)"/>
<use xlink:href="#DejaVuSans-16" transform="translate(186.96875 0)"/>
<use xlink:href="#DejaVuSans-15" transform="translate(250.59375 0)"/>
<use xlink:href="#DejaVuSans-10" transform="translate(314.21875 0)"/>
<use xlink:href="#DejaVuSans-36" transform="translate(350.296875 0)"/>
<use xlink:href="#DejaVuSans-16" transform="translate(413.78125 0)"/>
</g>
</g>
<g id="text_6">
<!-- 220 Ω -->
<g style="fill: #1f2937" transform="translate(251.557001 40.415231) scale(0.09 -0.09)">
<defs>
<path id="DejaVuSans-13" d="M 2034 4250
Q 1547 4250 1301 3770
Q 1056 3291 1056 2328
Q 1056 1369 1301 889
Q 1547 409 2034 409
Q 2525 409 2770 889
Q 3016 1369 3016 2328
Q 3016 3291 2770 3770
Q 2525 4250 2034 4250
z
M 2034 4750
Q 2819 4750 3233 4129
Q 3647 3509 3647 2328
Q 3647 1150 3233 529
Q 2819 -91 2034 -91
Q 1250 -91 836 529
Q 422 1150 422 2328
Q 422 3509 836 4129
Q 1250 4750 2034 4750
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-3" transform="scale(0.015625)"/>
<path id="DejaVuSans-33d" d="M 4647 556
L 4647 0
L 2772 0
L 2772 556
Q 3325 859 3634 1378
Q 3944 1897 3944 2528
Q 3944 3278 3531 3731
Q 3119 4184 2444 4184
Q 1769 4184 1355 3729
Q 941 3275 941 2528
Q 941 1897 1250 1378
Q 1563 859 2119 556
L 2119 0
L 244 0
L 244 556
L 1241 556
Q 747 991 519 1456
Q 294 1922 294 2497
Q 294 3491 894 4106
Q 1491 4722 2444 4722
Q 3391 4722 3994 4106
Q 4594 3494 4594 2528
Q 4594 1922 4372 1459
Q 4150 997 3647 556
L 4647 556
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-15"/>
<use xlink:href="#DejaVuSans-15" transform="translate(63.625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(127.25 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(190.875 0)"/>
<use xlink:href="#DejaVuSans-33d" transform="translate(222.65625 0)"/>
</g>
</g>
<g id="text_7">
<!-- 220 Ω -->
<g style="fill: #1f2937" transform="translate(251.557001 104.75031) scale(0.09 -0.09)">
<use xlink:href="#DejaVuSans-15"/>
<use xlink:href="#DejaVuSans-15" transform="translate(63.625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(127.25 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(190.875 0)"/>
<use xlink:href="#DejaVuSans-33d" transform="translate(222.65625 0)"/>
</g>
</g>
<g id="text_8">
<!-- 220 Ω -->
<g style="fill: #1f2937" transform="translate(251.557001 169.085388) scale(0.09 -0.09)">
<use xlink:href="#DejaVuSans-15"/>
<use xlink:href="#DejaVuSans-15" transform="translate(63.625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(127.25 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(190.875 0)"/>
<use xlink:href="#DejaVuSans-33d" transform="translate(222.65625 0)"/>
</g>
</g>
<g id="text_9">
<!-- 220 Ω -->
<g style="fill: #1f2937" transform="translate(251.557001 233.420467) scale(0.09 -0.09)">
<use xlink:href="#DejaVuSans-15"/>
<use xlink:href="#DejaVuSans-15" transform="translate(63.625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(127.25 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(190.875 0)"/>
<use xlink:href="#DejaVuSans-33d" transform="translate(222.65625 0)"/>
</g>
</g>
<g id="text_10">
<!-- GND -->
<g style="fill: #6b7280" transform="translate(447.702393 147.657021) scale(0.09 -0.09)">
<defs>
<path id="DejaVuSans-31" d="M 628 4666
L 1478 4666
L 3547 763
L 3547 4666
L 4159 4666
L 4159 0
L 3309 0
L 1241 3903
L 1241 0
L 628 0
L 628 4666
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-27" d="M 1259 4147
L 1259 519
L 2022 519
Q 2988 519 3436 956
Q 3884 1394 3884 2338
Q 3884 3275 3436 3711
Q 2988 4147 2022 4147
L 1259 4147
z
M 628 4666
L 1925 4666
Q 3281 4666 3915 4102
Q 4550 3538 4550 2338
Q 4550 1131 3912 565
Q 3275 0 1925 0
L 628 0
L 628 4666
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-2a"/>
<use xlink:href="#DejaVuSans-31" transform="translate(77.484375 0)"/>
<use xlink:href="#DejaVuSans-27" transform="translate(152.296875 0)"/>
</g>
<!-- commun -->
<g style="fill: #6b7280" transform="translate(438.322705 155.837177) scale(0.09 -0.09)">
<defs>
<path id="DejaVuSans-46" d="M 3122 3366
L 3122 2828
Q 2878 2963 2633 3030
Q 2388 3097 2138 3097
Q 1578 3097 1268 2742
Q 959 2388 959 1747
Q 959 1106 1268 751
Q 1578 397 2138 397
Q 2388 397 2633 464
Q 2878 531 3122 666
L 3122 134
Q 2881 22 2623 -34
Q 2366 -91 2075 -91
Q 1284 -91 818 406
Q 353 903 353 1747
Q 353 2603 823 3093
Q 1294 3584 2113 3584
Q 2378 3584 2631 3529
Q 2884 3475 3122 3366
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-52" d="M 1959 3097
Q 1497 3097 1228 2736
Q 959 2375 959 1747
Q 959 1119 1226 758
Q 1494 397 1959 397
Q 2419 397 2687 759
Q 2956 1122 2956 1747
Q 2956 2369 2687 2733
Q 2419 3097 1959 3097
z
M 1959 3584
Q 2709 3584 3137 3096
Q 3566 2609 3566 1747
Q 3566 888 3137 398
Q 2709 -91 1959 -91
Q 1206 -91 779 398
Q 353 888 353 1747
Q 353 2609 779 3096
Q 1206 3584 1959 3584
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-50" d="M 3328 2828
Q 3544 3216 3844 3400
Q 4144 3584 4550 3584
Q 5097 3584 5394 3201
Q 5691 2819 5691 2113
L 5691 0
L 5113 0
L 5113 2094
Q 5113 2597 4934 2840
Q 4756 3084 4391 3084
Q 3944 3084 3684 2787
Q 3425 2491 3425 1978
L 3425 0
L 2847 0
L 2847 2094
Q 2847 2600 2669 2842
Q 2491 3084 2119 3084
Q 1678 3084 1418 2786
Q 1159 2488 1159 1978
L 1159 0
L 581 0
L 581 3500
L 1159 3500
L 1159 2956
Q 1356 3278 1631 3431
Q 1906 3584 2284 3584
Q 2666 3584 2933 3390
Q 3200 3197 3328 2828
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-58" d="M 544 1381
L 544 3500
L 1119 3500
L 1119 1403
Q 1119 906 1312 657
Q 1506 409 1894 409
Q 2359 409 2629 706
Q 2900 1003 2900 1516
L 2900 3500
L 3475 3500
L 3475 0
L 2900 0
L 2900 538
Q 2691 219 2414 64
Q 2138 -91 1772 -91
Q 1169 -91 856 284
Q 544 659 544 1381
z
M 1991 3584
L 1991 3584
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-51" d="M 3513 2113
L 3513 0
L 2938 0
L 2938 2094
Q 2938 2591 2744 2837
Q 2550 3084 2163 3084
Q 1697 3084 1428 2787
Q 1159 2491 1159 1978
L 1159 0
L 581 0
L 581 3500
L 1159 3500
L 1159 2956
Q 1366 3272 1645 3428
Q 1925 3584 2291 3584
Q 2894 3584 3203 3211
Q 3513 2838 3513 2113
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-46"/>
<use xlink:href="#DejaVuSans-52" transform="translate(54.984375 0)"/>
<use xlink:href="#DejaVuSans-50" transform="translate(116.171875 0)"/>
<use xlink:href="#DejaVuSans-50" transform="translate(213.578125 0)"/>
<use xlink:href="#DejaVuSans-58" transform="translate(310.984375 0)"/>
<use xlink:href="#DejaVuSans-51" transform="translate(374.359375 0)"/>
</g>
</g>
</g>
</g>
<defs>
<clipPath id="p7e999a346a">
<rect x="7.2" y="7.2" width="460.8" height="311.341571"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 77 KiB

+63 -64
View File
@@ -6,7 +6,6 @@
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<cc:Work>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:date>2026-10-18T14:38:22.443124</dc:date>
<dc:format>image/svg+xml</dc:format>
<dc:creator>
<cc:Agent>
@@ -42,16 +41,16 @@ z
<g id="line2d_1">
<path d="M 34.563281 166.632956
L 34.563281 34.202148
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_2">
<defs>
<path id="m5a79eaf9fe" d="M 0 0
<path id="m5aaaf2f533" d="M 0 0
L 0 3.5
" style="stroke: #6b7280; stroke-width: 0.8"/>
</defs>
<g>
<use xlink:href="#m5a79eaf9fe" x="34.563281" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="34.563281" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_1">
@@ -88,11 +87,11 @@ z
<g id="line2d_3">
<path d="M 74.065022 166.632956
L 74.065022 34.202148
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_4">
<g>
<use xlink:href="#m5a79eaf9fe" x="74.065022" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="74.065022" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_2">
@@ -122,11 +121,11 @@ z
<g id="line2d_5">
<path d="M 113.566762 166.632956
L 113.566762 34.202148
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_6">
<g>
<use xlink:href="#m5a79eaf9fe" x="113.566762" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="113.566762" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_3">
@@ -166,11 +165,11 @@ z
<g id="line2d_7">
<path d="M 153.068502 166.632956
L 153.068502 34.202148
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_8">
<g>
<use xlink:href="#m5a79eaf9fe" x="153.068502" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="153.068502" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_4">
@@ -495,16 +494,16 @@ z
<g id="line2d_9">
<path d="M 34.563281 160.613374
L 164.919024 160.613374
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_10">
<defs>
<path id="me72c6334a5" d="M 0 0
<path id="mae80ff71dc" d="M 0 0
L -3.5 0
" style="stroke: #6b7280; stroke-width: 0.8"/>
</defs>
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="160.613374" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="160.613374" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_6">
@@ -518,11 +517,11 @@ L -3.5 0
<g id="line2d_11">
<path d="M 34.563281 143.414568
L 164.919024 143.414568
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_12">
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="143.414568" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="143.414568" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_7">
@@ -536,11 +535,11 @@ L 164.919024 143.414568
<g id="line2d_13">
<path d="M 34.563281 126.215762
L 164.919024 126.215762
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_14">
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="126.215762" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="126.215762" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_8">
@@ -554,11 +553,11 @@ L 164.919024 126.215762
<g id="line2d_15">
<path d="M 34.563281 109.016956
L 164.919024 109.016956
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_16">
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="109.016956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="109.016956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_9">
@@ -572,11 +571,11 @@ L 164.919024 109.016956
<g id="line2d_17">
<path d="M 34.563281 91.818149
L 164.919024 91.818149
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_18">
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="91.818149" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="91.818149" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_10">
@@ -611,11 +610,11 @@ z
<g id="line2d_19">
<path d="M 34.563281 74.619343
L 164.919024 74.619343
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_20">
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="74.619343" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="74.619343" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_11">
@@ -656,11 +655,11 @@ z
<g id="line2d_21">
<path d="M 34.563281 57.420537
L 164.919024 57.420537
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_22">
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="57.420537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="57.420537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_12">
@@ -706,11 +705,11 @@ z
<g id="line2d_23">
<path d="M 34.563281 40.221731
L 164.919024 40.221731
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_24">
<g>
<use xlink:href="#me72c6334a5" x="34.563281" y="40.221731" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="34.563281" y="40.221731" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_13">
@@ -826,7 +825,7 @@ z
<path d="M 34.563281 160.613374
L 164.919024 40.221731
L 164.919024 40.221731
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke-dasharray: 1.3,2.145; stroke-dashoffset: 0; stroke: #6b7280; stroke-width: 1.3"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke-dasharray: 1.3,2.145; stroke-dashoffset: 0; stroke: #6b7280; stroke-width: 1.3"/>
</g>
<g id="line2d_26">
<path d="M 34.563281 160.613374
@@ -846,7 +845,7 @@ L 148.738732 57.420537
L 148.738732 40.221731
L 164.919024 40.221731
L 164.919024 40.221731
" clip-path="url(#pb4ea67491b)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
" clip-path="url(#p4e42c2ff59)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
</g>
<g id="patch_3">
<path d="M 34.563281 166.632956
@@ -1725,16 +1724,16 @@ z
<g id="line2d_29">
<path d="M 397.941901 166.632956
L 397.941901 166.632956
" clip-path="url(#p9bc3014479)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb446d379c4)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_30">
<defs>
<path id="ma64c80e14d" d="M 0 0
<path id="m987850f8ba" d="M 0 0
L 3.5 0
" style="stroke: #e07a1f; stroke-width: 0.8"/>
</defs>
<g>
<use xlink:href="#ma64c80e14d" x="397.941901" y="166.632956" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
<use xlink:href="#m987850f8ba" x="397.941901" y="166.632956" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
</g>
</g>
<g id="text_19">
@@ -1748,11 +1747,11 @@ L 3.5 0
<g id="line2d_31">
<path d="M 397.941901 122.489354
L 397.941901 122.489354
" clip-path="url(#p9bc3014479)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb446d379c4)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_32">
<g>
<use xlink:href="#ma64c80e14d" x="397.941901" y="122.489354" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
<use xlink:href="#m987850f8ba" x="397.941901" y="122.489354" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
</g>
</g>
<g id="text_20">
@@ -1767,11 +1766,11 @@ L 397.941901 122.489354
<g id="line2d_33">
<path d="M 397.941901 78.345751
L 397.941901 78.345751
" clip-path="url(#p9bc3014479)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb446d379c4)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_34">
<g>
<use xlink:href="#ma64c80e14d" x="397.941901" y="78.345751" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
<use xlink:href="#m987850f8ba" x="397.941901" y="78.345751" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
</g>
</g>
<g id="text_21">
@@ -1787,11 +1786,11 @@ L 397.941901 78.345751
<g id="line2d_35">
<path d="M 397.941901 34.202148
L 397.941901 34.202148
" clip-path="url(#p9bc3014479)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb446d379c4)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_36">
<g>
<use xlink:href="#ma64c80e14d" x="397.941901" y="34.202148" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
<use xlink:href="#m987850f8ba" x="397.941901" y="34.202148" style="fill: #e07a1f; stroke: #e07a1f; stroke-width: 0.8"/>
</g>
</g>
<g id="text_22">
@@ -1878,11 +1877,11 @@ L 397.941901 34.202148
<g id="line2d_37">
<path d="M 234.997222 166.632956
L 234.997222 34.202148
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_38">
<g>
<use xlink:href="#m5a79eaf9fe" x="234.997222" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="234.997222" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_24">
@@ -1896,11 +1895,11 @@ L 234.997222 34.202148
<g id="line2d_39">
<path d="M 284.374398 166.632956
L 284.374398 34.202148
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_40">
<g>
<use xlink:href="#m5a79eaf9fe" x="284.374398" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="284.374398" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_25">
@@ -1914,11 +1913,11 @@ L 284.374398 34.202148
<g id="line2d_41">
<path d="M 333.751573 166.632956
L 333.751573 34.202148
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_42">
<g>
<use xlink:href="#m5a79eaf9fe" x="333.751573" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="333.751573" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_26">
@@ -1932,11 +1931,11 @@ L 333.751573 34.202148
<g id="line2d_43">
<path d="M 383.128748 166.632956
L 383.128748 34.202148
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_44">
<g>
<use xlink:href="#m5a79eaf9fe" x="383.128748" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="383.128748" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_27">
@@ -1977,11 +1976,11 @@ L 383.128748 34.202148
<g id="line2d_45">
<path d="M 234.997222 166.632956
L 397.941901 166.632956
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_46">
<g>
<use xlink:href="#me72c6334a5" x="234.997222" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="234.997222" y="166.632956" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_29">
@@ -1995,11 +1994,11 @@ L 397.941901 166.632956
<g id="line2d_47">
<path d="M 234.997222 133.51717
L 397.941901 133.51717
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_48">
<g>
<use xlink:href="#me72c6334a5" x="234.997222" y="133.51717" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="234.997222" y="133.51717" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_30">
@@ -2016,11 +2015,11 @@ L 397.941901 133.51717
<g id="line2d_49">
<path d="M 234.997222 100.401383
L 397.941901 100.401383
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_50">
<g>
<use xlink:href="#me72c6334a5" x="234.997222" y="100.401383" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="234.997222" y="100.401383" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_31">
@@ -2037,11 +2036,11 @@ L 397.941901 100.401383
<g id="line2d_51">
<path d="M 234.997222 67.285596
L 397.941901 67.285596
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_52">
<g>
<use xlink:href="#me72c6334a5" x="234.997222" y="67.285596" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="234.997222" y="67.285596" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_32">
@@ -2058,11 +2057,11 @@ L 397.941901 67.285596
<g id="line2d_53">
<path d="M 234.997222 34.202148
L 397.941901 34.202148
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_54">
<g>
<use xlink:href="#me72c6334a5" x="234.997222" y="34.202148" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="234.997222" y="34.202148" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_33">
@@ -2160,17 +2159,17 @@ z
<g id="line2d_55">
<path d="M 234.997222 166.632956
L 397.941901 34.202148
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
</g>
<g id="line2d_56">
<path d="M 316.469562 166.632956
L 316.469562 100.401383
L 397.941901 100.401383
" clip-path="url(#pc59e8ca2fc)" style="fill: none; stroke-dasharray: 4.44,1.92; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.2"/>
" clip-path="url(#pb34704d88d)" style="fill: none; stroke-dasharray: 4.44,1.92; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.2"/>
</g>
<g id="line2d_57">
<defs>
<path id="mfaa11af312" d="M 0 3
<path id="m254a30c17c" d="M 0 3
C 0.795609 3 1.55874 2.683901 2.12132 2.12132
C 2.683901 1.55874 3 0.795609 3 0
C 3 -0.795609 2.683901 -1.55874 2.12132 -2.12132
@@ -2182,8 +2181,8 @@ C -1.55874 2.683901 -0.795609 3 0 3
z
" style="stroke: #e07a1f"/>
</defs>
<g clip-path="url(#pc59e8ca2fc)">
<use xlink:href="#mfaa11af312" x="316.469562" y="100.401383" style="fill: #e07a1f; stroke: #e07a1f"/>
<g clip-path="url(#pb34704d88d)">
<use xlink:href="#m254a30c17c" x="316.469562" y="100.401383" style="fill: #e07a1f; stroke: #e07a1f"/>
</g>
</g>
<g id="patch_8">
@@ -2622,13 +2621,13 @@ z
</g>
</g>
<defs>
<clipPath id="pb4ea67491b">
<clipPath id="p4e42c2ff59">
<rect x="34.563281" y="34.202148" width="130.355743" height="132.430808"/>
</clipPath>
<clipPath id="p9bc3014479">
<clipPath id="pb446d379c4">
<rect x="397.941901" y="34.202148" width="0" height="132.430808"/>
</clipPath>
<clipPath id="pc59e8ca2fc">
<clipPath id="pb34704d88d">
<rect x="234.997222" y="34.202148" width="162.944679" height="132.430808"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

+22 -23
View File
@@ -6,7 +6,6 @@
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<cc:Work>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:date>2026-10-18T14:38:22.285543</dc:date>
<dc:format>image/svg+xml</dc:format>
<dc:creator>
<cc:Agent>
@@ -35,7 +34,7 @@ L 437.451525 174.665381
L 430.108684 164.385404
L 444.794366 164.385404
z
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_1">
<path d="M 129.052194 147.496869
@@ -43,39 +42,39 @@ L 319.966065 147.496869
L 319.966065 24.137136
L 129.052194 24.137136
L 129.052194 147.496869
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_2">
<path d="M 129.052194 132.811186
L 114.366512 132.811186
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_3">
<path d="M 129.052194 85.817003
L 114.366512 85.817003
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_4">
<path d="M 129.052194 38.822819
L 114.366512 38.822819
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_5">
<path d="M 319.966065 132.811186
L 334.651748 132.811186
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_6">
<path d="M 114.366512 38.822819
L 82.05801 38.822819
L 49.749509 38.822819
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_7">
<path d="M 114.366512 132.811186
L 82.05801 132.811186
L 49.749509 132.811186
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_8">
<path d="M 49.749509 38.822819
@@ -88,24 +87,24 @@ L 57.09235 93.159844
L 42.406668 98.055071
L 49.749509 100.502685
L 49.749509 132.811186
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_9">
<path d="M 70.896892 85.817003
L 64.435191 85.817003
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_3">
<path d="M 64.435191 88.019855
L 57.09235 85.817003
L 64.435191 83.61415
z
" clip-path="url(#p34353fba05)" style="fill: #2e8b57"/>
" clip-path="url(#p4639aee0bc)" style="fill: #2e8b57"/>
</g>
<g id="line2d_10">
<path d="M 70.896892 85.817003
L 114.366512 85.817003
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #2e8b57; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_11">
<path d="M 49.749509 132.811186
@@ -116,13 +115,13 @@ M 44.60952 148.084296
L 54.889498 148.084296
M 48.280941 151.60886
L 51.218077 151.60886
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_12">
<path d="M 334.651748 132.811186
L 349.33743 132.811186
L 364.023113 132.811186
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_13">
<path d="M 364.023113 132.811186
@@ -135,7 +134,7 @@ L 408.08016 125.468345
L 412.975387 140.154028
L 415.423001 132.811186
L 437.451525 132.811186
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_14">
<path d="M 437.451525 132.811186
@@ -144,31 +143,31 @@ M 444.794366 174.665381
L 430.108684 174.665381
M 437.451525 174.665381
L 437.451525 206.239598
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="line2d_15">
<path d="M 448.465787 171.728245
L 456.215461 176.15663
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_4">
<path d="M 457.381244 174.116512
L 461.315759 179.071086
L 455.049679 178.196749
z
" clip-path="url(#p34353fba05)" style="fill: #e07a1f"/>
" clip-path="url(#p4639aee0bc)" style="fill: #e07a1f"/>
</g>
<g id="line2d_16">
<path d="M 448.465787 165.119688
L 456.215461 169.548073
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #e07a1f; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="patch_5">
<path d="M 457.381244 167.507954
L 461.315759 172.462529
L 455.049679 171.588192
z
" clip-path="url(#p34353fba05)" style="fill: #e07a1f"/>
" clip-path="url(#p4639aee0bc)" style="fill: #e07a1f"/>
</g>
<g id="line2d_17">
<path d="M 437.451525 206.239598
@@ -179,7 +178,7 @@ M 432.311536 221.512708
L 442.591514 221.512708
M 435.982956 225.037272
L 438.920093 225.037272
" clip-path="url(#p34353fba05)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
" clip-path="url(#p4639aee0bc)" style="fill: none; stroke: #1f2937; stroke-width: 2; stroke-linecap: round"/>
</g>
<g id="text_1">
<!-- GND -->
@@ -713,7 +712,7 @@ z
</g>
</g>
<defs>
<clipPath id="p34353fba05">
<clipPath id="p4639aee0bc">
<rect x="21.461213" y="14.09213" width="460.8" height="220.990149"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+44 -45
View File
@@ -6,7 +6,6 @@
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<cc:Work>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:date>2026-10-18T14:38:21.884270</dc:date>
<dc:format>image/svg+xml</dc:format>
<dc:creator>
<cc:Agent>
@@ -39,7 +38,7 @@ z
</g>
<g id="FillBetweenPolyCollection_1">
<defs>
<path id="m03540ac234" d="M 56.591778 -218.117371
<path id="m08f342c75d" d="M 56.591778 -218.117371
L 56.591778 -262.749541
L 56.756559 -262.749541
L 56.921341 -262.749541
@@ -4044,41 +4043,41 @@ L 56.591778 -218.117371
z
" style="stroke: #1f5fa8; stroke-opacity: 0.12"/>
</defs>
<g clip-path="url(#pb27fd7aba9)">
<use xlink:href="#m03540ac234" x="0" y="304.367899" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
<g clip-path="url(#p1988a58d21)">
<use xlink:href="#m08f342c75d" x="0" y="304.367899" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
</g>
</g>
<g id="matplotlib.axis_1">
<g id="xtick_1">
<g id="line2d_1">
<defs>
<path id="m04ce1f384c" d="M 0 0
<path id="m5aaaf2f533" d="M 0 0
L 0 3.5
" style="stroke: #6b7280; stroke-width: 0.8"/>
</defs>
<g>
<use xlink:href="#m04ce1f384c" x="56.591778" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="56.591778" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_2">
<g id="line2d_2">
<g>
<use xlink:href="#m04ce1f384c" x="166.446056" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="166.446056" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_3">
<g id="line2d_3">
<g>
<use xlink:href="#m04ce1f384c" x="276.300335" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="276.300335" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_4">
<g id="line2d_4">
<g>
<use xlink:href="#m04ce1f384c" x="386.154614" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="386.154614" y="90.307997" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
@@ -4088,16 +4087,16 @@ L 0 3.5
<g id="line2d_5">
<path d="M 40.121875 86.250527
L 402.459735 86.250527
" clip-path="url(#pb27fd7aba9)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p1988a58d21)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_6">
<defs>
<path id="mf7ab9066d0" d="M 0 0
<path id="mae80ff71dc" d="M 0 0
L -3.5 0
" style="stroke: #6b7280; stroke-width: 0.8"/>
</defs>
<g>
<use xlink:href="#mf7ab9066d0" x="40.121875" y="86.250527" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="86.250527" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_1">
@@ -4147,11 +4146,11 @@ z
<g id="line2d_7">
<path d="M 40.121875 41.618357
L 402.459735 41.618357
" clip-path="url(#pb27fd7aba9)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p1988a58d21)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_8">
<g>
<use xlink:href="#mf7ab9066d0" x="40.121875" y="41.618357" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="41.618357" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_2">
@@ -4223,12 +4222,12 @@ L 303.599124 41.618357
L 303.763905 86.250527
L 385.989833 86.250527
L 385.989833 86.250527
" clip-path="url(#pb27fd7aba9)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
" clip-path="url(#p1988a58d21)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
</g>
<g id="line2d_10">
<path d="M 40.121875 75.092485
L 402.459735 75.092485
" clip-path="url(#pb27fd7aba9)" style="fill: none; stroke-dasharray: 6.66,2.88; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.8"/>
" clip-path="url(#p1988a58d21)" style="fill: none; stroke-dasharray: 6.66,2.88; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.8"/>
</g>
<g id="patch_3">
<path d="M 40.121875 90.307997
@@ -5299,7 +5298,7 @@ z
</g>
<g id="FillBetweenPolyCollection_2">
<defs>
<path id="m28b6650527" d="M 56.591778 -130.387811
<path id="md990bfe4d7" d="M 56.591778 -130.387811
L 56.591778 -175.019981
L 56.756559 -175.019981
L 56.921341 -175.019981
@@ -9304,36 +9303,36 @@ L 56.591778 -130.387811
z
" style="stroke: #1f5fa8; stroke-opacity: 0.12"/>
</defs>
<g clip-path="url(#p0a19abac77)">
<use xlink:href="#m28b6650527" x="0" y="304.367899" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
<g clip-path="url(#paaff85ab5e)">
<use xlink:href="#md990bfe4d7" x="0" y="304.367899" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
</g>
</g>
<g id="matplotlib.axis_3">
<g id="xtick_5">
<g id="line2d_11">
<g>
<use xlink:href="#m04ce1f384c" x="56.591778" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="56.591778" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_6">
<g id="line2d_12">
<g>
<use xlink:href="#m04ce1f384c" x="166.446056" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="166.446056" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_7">
<g id="line2d_13">
<g>
<use xlink:href="#m04ce1f384c" x="276.300335" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="276.300335" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_8">
<g id="line2d_14">
<g>
<use xlink:href="#m04ce1f384c" x="386.154614" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="386.154614" y="178.037557" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
@@ -9343,11 +9342,11 @@ z
<g id="line2d_15">
<path d="M 40.121875 173.980087
L 402.459735 173.980087
" clip-path="url(#p0a19abac77)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#paaff85ab5e)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_16">
<g>
<use xlink:href="#mf7ab9066d0" x="40.121875" y="173.980087" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="173.980087" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_7">
@@ -9363,11 +9362,11 @@ L 402.459735 173.980087
<g id="line2d_17">
<path d="M 40.121875 129.347917
L 402.459735 129.347917
" clip-path="url(#p0a19abac77)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#paaff85ab5e)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_18">
<g>
<use xlink:href="#mf7ab9066d0" x="40.121875" y="129.347917" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="129.347917" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_8">
@@ -9396,12 +9395,12 @@ L 331.11762 129.347917
L 331.282402 173.980087
L 385.989833 173.980087
L 385.989833 173.980087
" clip-path="url(#p0a19abac77)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
" clip-path="url(#paaff85ab5e)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
</g>
<g id="line2d_20">
<path d="M 40.121875 151.664002
L 402.459735 151.664002
" clip-path="url(#p0a19abac77)" style="fill: none; stroke-dasharray: 6.66,2.88; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.8"/>
" clip-path="url(#paaff85ab5e)" style="fill: none; stroke-dasharray: 6.66,2.88; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.8"/>
</g>
<g id="patch_8">
<path d="M 40.121875 178.037557
@@ -9514,7 +9513,7 @@ z
</g>
<g id="FillBetweenPolyCollection_3">
<defs>
<path id="m2b82f9c8a9" d="M 56.591778 -42.658251
<path id="m6cd24ff94b" d="M 56.591778 -42.658251
L 56.591778 -87.290421
L 56.756559 -87.290421
L 56.921341 -87.290421
@@ -13519,15 +13518,15 @@ L 56.591778 -42.658251
z
" style="stroke: #1f5fa8; stroke-opacity: 0.12"/>
</defs>
<g clip-path="url(#p83c9427cba)">
<use xlink:href="#m2b82f9c8a9" x="0" y="304.367899" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
<g clip-path="url(#p8d88850880)">
<use xlink:href="#m6cd24ff94b" x="0" y="304.367899" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
</g>
</g>
<g id="matplotlib.axis_5">
<g id="xtick_9">
<g id="line2d_21">
<g>
<use xlink:href="#m04ce1f384c" x="56.591778" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="56.591778" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_11">
@@ -13540,7 +13539,7 @@ z
<g id="xtick_10">
<g id="line2d_22">
<g>
<use xlink:href="#m04ce1f384c" x="166.446056" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="166.446056" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_12">
@@ -13553,7 +13552,7 @@ z
<g id="xtick_11">
<g id="line2d_23">
<g>
<use xlink:href="#m04ce1f384c" x="276.300335" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="276.300335" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_13">
@@ -13593,7 +13592,7 @@ z
<g id="xtick_12">
<g id="line2d_24">
<g>
<use xlink:href="#m04ce1f384c" x="386.154614" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="386.154614" y="265.767117" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_14">
@@ -13875,11 +13874,11 @@ z
<g id="line2d_25">
<path d="M 40.121875 261.709647
L 402.459735 261.709647
" clip-path="url(#p83c9427cba)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p8d88850880)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_26">
<g>
<use xlink:href="#mf7ab9066d0" x="40.121875" y="261.709647" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="261.709647" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_16">
@@ -13895,11 +13894,11 @@ L 402.459735 261.709647
<g id="line2d_27">
<path d="M 40.121875 217.077477
L 402.459735 217.077477
" clip-path="url(#p83c9427cba)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p8d88850880)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_28">
<g>
<use xlink:href="#mf7ab9066d0" x="40.121875" y="217.077477" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="217.077477" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_17">
@@ -13928,12 +13927,12 @@ L 358.636117 217.077477
L 358.800899 261.709647
L 385.989833 261.709647
L 385.989833 261.709647
" clip-path="url(#p83c9427cba)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
" clip-path="url(#p8d88850880)" style="fill: none; stroke: #1f5fa8; stroke-width: 2; stroke-linecap: square"/>
</g>
<g id="line2d_30">
<path d="M 40.121875 228.23552
L 402.459735 228.23552
" clip-path="url(#p83c9427cba)" style="fill: none; stroke-dasharray: 6.66,2.88; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.8"/>
" clip-path="url(#p8d88850880)" style="fill: none; stroke-dasharray: 6.66,2.88; stroke-dashoffset: 0; stroke: #e07a1f; stroke-width: 1.8"/>
</g>
<g id="patch_11">
<path d="M 40.121875 265.767117
@@ -14063,13 +14062,13 @@ z
</g>
</g>
<defs>
<clipPath id="pb27fd7aba9">
<clipPath id="p1988a58d21">
<rect x="40.121875" y="30.798437" width="362.33786" height="59.50956"/>
</clipPath>
<clipPath id="p0a19abac77">
<clipPath id="paaff85ab5e">
<rect x="40.121875" y="118.527997" width="362.33786" height="59.50956"/>
</clipPath>
<clipPath id="p83c9427cba">
<clipPath id="p8d88850880">
<rect x="40.121875" y="206.257557" width="362.33786" height="59.50956"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 357 KiB

+56 -57
View File
@@ -6,7 +6,6 @@
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<cc:Work>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:date>2026-10-18T14:38:22.123535</dc:date>
<dc:format>image/svg+xml</dc:format>
<dc:creator>
<cc:Agent>
@@ -39,7 +38,7 @@ z
</g>
<g id="FillBetweenPolyCollection_1">
<defs>
<path id="m659a75ebc5" d="M 58.225414 -192.810731
<path id="m1554448120" d="M 58.225414 -192.810731
L 58.225414 -236.58288
L 58.315954 -236.58288
L 58.406495 -236.58288
@@ -8044,76 +8043,76 @@ L 58.225414 -192.810731
z
" style="stroke: #1f5fa8; stroke-opacity: 0.12"/>
</defs>
<g clip-path="url(#pc2960f74f9)">
<use xlink:href="#m659a75ebc5" x="0" y="261.76537" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
<g clip-path="url(#p542e91d944)">
<use xlink:href="#m1554448120" x="0" y="261.76537" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
</g>
</g>
<g id="matplotlib.axis_1">
<g id="xtick_1">
<g id="line2d_1">
<defs>
<path id="m392e25bdc6" d="M 0 0
<path id="m5aaaf2f533" d="M 0 0
L 0 3.5
" style="stroke: #6b7280; stroke-width: 0.8"/>
</defs>
<g>
<use xlink:href="#m392e25bdc6" x="58.225414" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="58.225414" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_2">
<g id="line2d_2">
<g>
<use xlink:href="#m392e25bdc6" x="103.49558" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="103.49558" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_3">
<g id="line2d_3">
<g>
<use xlink:href="#m392e25bdc6" x="148.765745" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="148.765745" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_4">
<g id="line2d_4">
<g>
<use xlink:href="#m392e25bdc6" x="194.03591" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="194.03591" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_5">
<g id="line2d_5">
<g>
<use xlink:href="#m392e25bdc6" x="239.306076" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="239.306076" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_6">
<g id="line2d_6">
<g>
<use xlink:href="#m392e25bdc6" x="284.576241" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="284.576241" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_7">
<g id="line2d_7">
<g>
<use xlink:href="#m392e25bdc6" x="329.846407" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="329.846407" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_8">
<g id="line2d_8">
<g>
<use xlink:href="#m392e25bdc6" x="375.116572" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="375.116572" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_9">
<g id="line2d_9">
<g>
<use xlink:href="#m392e25bdc6" x="420.386737" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="420.386737" y="72.933925" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
@@ -8123,16 +8122,16 @@ L 0 3.5
<g id="line2d_10">
<path d="M 40.121875 68.954639
L 438.399736 68.954639
" clip-path="url(#pc2960f74f9)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p542e91d944)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_11">
<defs>
<path id="mf6ae75bd23" d="M 0 0
<path id="mae80ff71dc" d="M 0 0
L -3.5 0
" style="stroke: #6b7280; stroke-width: 0.8"/>
</defs>
<g>
<use xlink:href="#mf6ae75bd23" x="40.121875" y="68.954639" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="68.954639" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_1">
@@ -8182,11 +8181,11 @@ z
<g id="line2d_12">
<path d="M 40.121875 25.18249
L 438.399736 25.18249
" clip-path="url(#pc2960f74f9)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p542e91d944)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_13">
<g>
<use xlink:href="#mf6ae75bd23" x="40.121875" y="25.18249" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="25.18249" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_2">
@@ -8254,7 +8253,7 @@ L 248.269569 25.18249
L 248.450649 68.954639
L 420.296197 68.954639
L 420.296197 68.954639
" clip-path="url(#pc2960f74f9)" style="fill: none; stroke: #1f5fa8; stroke-width: 1.8; stroke-linecap: square"/>
" clip-path="url(#p542e91d944)" style="fill: none; stroke: #1f5fa8; stroke-width: 1.8; stroke-linecap: square"/>
</g>
<g id="patch_3">
<path d="M 40.121875 72.933925
@@ -9050,7 +9049,7 @@ z
</g>
<g id="FillBetweenPolyCollection_2">
<defs>
<path id="m1ecb7d6aaa" d="M 58.225414 -117.495009
<path id="mc1f166ca78" d="M 58.225414 -117.495009
L 58.225414 -161.267158
L 58.315954 -161.267158
L 58.406495 -161.267158
@@ -17055,71 +17054,71 @@ L 58.225414 -117.495009
z
" style="stroke: #1f5fa8; stroke-opacity: 0.12"/>
</defs>
<g clip-path="url(#pfd1aaeace0)">
<use xlink:href="#m1ecb7d6aaa" x="0" y="261.76537" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
<g clip-path="url(#pd7fc223f79)">
<use xlink:href="#mc1f166ca78" x="0" y="261.76537" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
</g>
</g>
<g id="matplotlib.axis_3">
<g id="xtick_10">
<g id="line2d_15">
<g>
<use xlink:href="#m392e25bdc6" x="58.225414" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="58.225414" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_11">
<g id="line2d_16">
<g>
<use xlink:href="#m392e25bdc6" x="103.49558" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="103.49558" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_12">
<g id="line2d_17">
<g>
<use xlink:href="#m392e25bdc6" x="148.765745" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="148.765745" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_13">
<g id="line2d_18">
<g>
<use xlink:href="#m392e25bdc6" x="194.03591" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="194.03591" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_14">
<g id="line2d_19">
<g>
<use xlink:href="#m392e25bdc6" x="239.306076" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="239.306076" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_15">
<g id="line2d_20">
<g>
<use xlink:href="#m392e25bdc6" x="284.576241" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="284.576241" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_16">
<g id="line2d_21">
<g>
<use xlink:href="#m392e25bdc6" x="329.846407" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="329.846407" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_17">
<g id="line2d_22">
<g>
<use xlink:href="#m392e25bdc6" x="375.116572" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="375.116572" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
<g id="xtick_18">
<g id="line2d_23">
<g>
<use xlink:href="#m392e25bdc6" x="420.386737" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="420.386737" y="148.249648" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
</g>
@@ -17129,11 +17128,11 @@ z
<g id="line2d_24">
<path d="M 40.121875 144.270361
L 438.399736 144.270361
" clip-path="url(#pfd1aaeace0)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pd7fc223f79)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_25">
<g>
<use xlink:href="#mf6ae75bd23" x="40.121875" y="144.270361" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="144.270361" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_5">
@@ -17149,11 +17148,11 @@ L 438.399736 144.270361
<g id="line2d_26">
<path d="M 40.121875 100.498213
L 438.399736 100.498213
" clip-path="url(#pfd1aaeace0)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#pd7fc223f79)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_27">
<g>
<use xlink:href="#mf6ae75bd23" x="40.121875" y="100.498213" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="100.498213" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_6">
@@ -17178,7 +17177,7 @@ L 252.796585 100.498213
L 252.977666 144.270361
L 420.296197 144.270361
L 420.296197 144.270361
" clip-path="url(#pfd1aaeace0)" style="fill: none; stroke: #1f5fa8; stroke-width: 1.8; stroke-linecap: square"/>
" clip-path="url(#pd7fc223f79)" style="fill: none; stroke: #1f5fa8; stroke-width: 1.8; stroke-linecap: square"/>
</g>
<g id="patch_7">
<path d="M 40.121875 148.249648
@@ -17295,7 +17294,7 @@ z
</g>
<g id="FillBetweenPolyCollection_3">
<defs>
<path id="m776958fd6c" d="M 58.225414 -42.179286
<path id="md342a5f774" d="M 58.225414 -42.179286
L 58.225414 -85.951435
L 58.315954 -85.951435
L 58.406495 -85.951435
@@ -25300,15 +25299,15 @@ L 58.225414 -42.179286
z
" style="stroke: #1f5fa8; stroke-opacity: 0.12"/>
</defs>
<g clip-path="url(#peefe64885e)">
<use xlink:href="#m776958fd6c" x="0" y="261.76537" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
<g clip-path="url(#p3b8cd38529)">
<use xlink:href="#md342a5f774" x="0" y="261.76537" style="fill: #1f5fa8; fill-opacity: 0.12; stroke: #1f5fa8; stroke-opacity: 0.12"/>
</g>
</g>
<g id="matplotlib.axis_5">
<g id="xtick_19">
<g id="line2d_29">
<g>
<use xlink:href="#m392e25bdc6" x="58.225414" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="58.225414" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_8">
@@ -25321,7 +25320,7 @@ z
<g id="xtick_20">
<g id="line2d_30">
<g>
<use xlink:href="#m392e25bdc6" x="103.49558" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="103.49558" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_9">
@@ -25334,7 +25333,7 @@ z
<g id="xtick_21">
<g id="line2d_31">
<g>
<use xlink:href="#m392e25bdc6" x="148.765745" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="148.765745" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_10">
@@ -25348,7 +25347,7 @@ z
<g id="xtick_22">
<g id="line2d_32">
<g>
<use xlink:href="#m392e25bdc6" x="194.03591" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="194.03591" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_11">
@@ -25362,7 +25361,7 @@ z
<g id="xtick_23">
<g id="line2d_33">
<g>
<use xlink:href="#m392e25bdc6" x="239.306076" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="239.306076" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_12">
@@ -25402,7 +25401,7 @@ z
<g id="xtick_24">
<g id="line2d_34">
<g>
<use xlink:href="#m392e25bdc6" x="284.576241" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="284.576241" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_13">
@@ -25416,7 +25415,7 @@ z
<g id="xtick_25">
<g id="line2d_35">
<g>
<use xlink:href="#m392e25bdc6" x="329.846407" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="329.846407" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_14">
@@ -25430,7 +25429,7 @@ z
<g id="xtick_26">
<g id="line2d_36">
<g>
<use xlink:href="#m392e25bdc6" x="375.116572" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="375.116572" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_15">
@@ -25444,7 +25443,7 @@ z
<g id="xtick_27">
<g id="line2d_37">
<g>
<use xlink:href="#m392e25bdc6" x="420.386737" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#m5aaaf2f533" x="420.386737" y="223.56537" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_16">
@@ -25571,11 +25570,11 @@ z
<g id="line2d_38">
<path d="M 40.121875 219.586084
L 438.399736 219.586084
" clip-path="url(#peefe64885e)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p3b8cd38529)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_39">
<g>
<use xlink:href="#mf6ae75bd23" x="40.121875" y="219.586084" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="219.586084" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_18">
@@ -25591,11 +25590,11 @@ L 438.399736 219.586084
<g id="line2d_40">
<path d="M 40.121875 175.813935
L 438.399736 175.813935
" clip-path="url(#peefe64885e)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
" clip-path="url(#p3b8cd38529)" style="fill: none; stroke: #e5e7eb; stroke-width: 0.8; stroke-linecap: square"/>
</g>
<g id="line2d_41">
<g>
<use xlink:href="#mf6ae75bd23" x="40.121875" y="175.813935" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
<use xlink:href="#mae80ff71dc" x="40.121875" y="175.813935" style="fill: #6b7280; stroke: #6b7280; stroke-width: 0.8"/>
</g>
</g>
<g id="text_19">
@@ -25620,7 +25619,7 @@ L 257.323602 175.813935
L 257.504682 219.586084
L 420.296197 219.586084
L 420.296197 219.586084
" clip-path="url(#peefe64885e)" style="fill: none; stroke: #1f5fa8; stroke-width: 1.8; stroke-linecap: square"/>
" clip-path="url(#p3b8cd38529)" style="fill: none; stroke: #1f5fa8; stroke-width: 1.8; stroke-linecap: square"/>
</g>
<g id="patch_10">
<path d="M 40.121875 223.56537
@@ -25713,13 +25712,13 @@ z
</g>
</g>
<defs>
<clipPath id="pc2960f74f9">
<clipPath id="p542e91d944">
<rect x="40.121875" y="11.918202" width="398.277861" height="61.015723"/>
</clipPath>
<clipPath id="pfd1aaeace0">
<clipPath id="pd7fc223f79">
<rect x="40.121875" y="87.233925" width="398.277861" height="61.015723"/>
</clipPath>
<clipPath id="peefe64885e">
<clipPath id="p3b8cd38529">
<rect x="40.121875" y="162.549648" width="398.277861" height="61.015723"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 644 KiB

After

Width:  |  Height:  |  Size: 644 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 48 KiB

Some files were not shown because too many files have changed in this diff Show More