From 6a13bfc2980f7e0cba46b85024ecf93b92393882 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 21:39:42 +0200 Subject: [PATCH 01/12] =?UTF-8?q?docs:=20sp=C3=A9cification=20du=20module?= =?UTF-8?q?=20additionnel=20Arduino?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-10-15-module-arduino-design.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-15-module-arduino-design.md diff --git a/docs/superpowers/specs/2026-10-15-module-arduino-design.md b/docs/superpowers/specs/2026-10-15-module-arduino-design.md new file mode 100644 index 0000000..b032ba2 --- /dev/null +++ b/docs/superpowers/specs/2026-10-15-module-arduino-design.md @@ -0,0 +1,99 @@ +# Module additionnel Arduino (PlatformIO, tests, CI) -- Design + +Date : 2026-10-15 +Statut : validé + +## Contexte + +Les lecteurs visés connaissent déjà Arduino, mais seulement à travers +l'IDE officiel et des fichiers `.ino`. Le module leur montre comment +travailler sur un microcontrôleur avec les pratiques de l'industrie : +PlatformIO, découpage du code pour le rendre testable, tests unitaires, +analyse statique et intégration continue. + +Le module s'appuie sur un projet réel et public : +`sketches/XIAO-ESP32-S3/DS18B20_MQTT` du dépôt +https://github.com/pguinet/arduino. Une sonde de température DS18B20, +branchée sur une Seeed Studio XIAO ESP32-S3, publie ses mesures dans Home +Assistant par MQTT Discovery. + +## Décisions + +1. **Un module de lecture personnelle, pas un cours en séance.** On n'a + donc aucune contrainte liée au CID (freeze, VirtualBox, USB dans la + VM). Le lecteur installe PlatformIO sur son propre PC : VS Code avec + l'extension, ou la CLI `pio`. Linux est l'exemple principal ; les + différences avec Windows (port COM) et macOS sont signalées. +2. **Pas de matériel obligatoire.** Les tests natifs, la compilation et + cppcheck tournent sans carte. Seuls le flashage et la publication MQTT + demandent une carte, ainsi qu'un broker. On le dit dès le chapitre 1. +3. **Le code reste dans le dépôt `pguinet/arduino`.** Le module en cite + des extraits commentés. Les liens pointent vers un commit figé + (`1af068f`) pour que le texte et le code ne divergent pas. On ne copie + pas le projet dans `formation_linux`. +4. **MQTT et Home Assistant sont présentés juste assez pour comprendre le + code.** Un encadré renvoie au futur homelab du club, où un broker et un + Home Assistant pourront être installés. +5. **Pas de dossier de TP séparé.** Les exercices sont dans les chapitres + et se font sans carte, sauf mention contraire. +6. **Prérequis** : avoir déjà utilisé l'IDE Arduino, et avoir lu le module + additionnel Git (renvoi pour les commits, le fork et GitHub). Durée + de lecture : 6 à 8 heures. + +## Fichiers + +``` +supports/modules_additionnels/module_arduino/ + 01_arduino_xiao_esp32s3.md + 02_ide_vers_platformio.md + 03_mqtt_home_assistant.md + 04_lecture_code.md + 05_tests_unitaires.md + 06_qualite_integration_continue.md +``` + +- `pdf/documents.yaml` : nouvelle entrée `module_additionnel_arduino.pdf` + (titre « Arduino : du sketch à l'intégration continue », sous-titre + « Module additionnel »), placée après Docker. +- `pdf/tests/test_documents.py` : nom ajouté dans `ATTENDUS`, à la même + position. +- `CLAUDE.md` et `README.md` : le module apparaît dans le plan et dans + l'arborescence. + +## Contenu des chapitres + +| Chapitre | Contenu | +|---|---| +| Arduino 1 : de l'Arduino à la XIAO ESP32-S3 | L'écosystème Arduino (framework et cartes), l'ESP32-S3, Seeed Studio et la XIAO (antenne U.FL obligatoire, USB natif, `303a:1001`). Le projet en bref : DS18B20, bus 1-Wire, pull-up interne, schéma de câblage en ASCII. Ce qu'on peut faire sans carte | +| Arduino 2 : de l'IDE à PlatformIO | Ce que cache un `.ino` (prototypes générés, `#include ` implicite, bibliothèques globales non versionnées). Installation (VS Code et extension, ou CLI). Arborescence `src/lib/test`, `platformio.ini` (plateforme, carte, environnements `xiao` et `native`, `lib_deps` versionnées). `pio run`, `-t upload`, `pio device monitor` | +| Arduino 3 : MQTT et Home Assistant | Broker, client, sujets, QoS en bref, messages retenus, testament (LWT). MQTT Discovery : le JSON de configuration, `availability_mode: all`. Le tableau des sujets du projet. Encadré homelab | +| Arduino 4 : lecture du code | L'architecture : code matériel dans `src/main.cpp`, logique pure dans `lib/temp_logic`. Boucle non bloquante (`millis()` contre `delay()`, débordement géré par la soustraction non signée, conversion DS18B20 asynchrone). Validation des lectures (-127, 85), `PublishPolicy`, `ProbeHealth`. Reconnexion WiFi et MQTT. Secrets (`credentials.h`, `.example`, `#error`) | +| Arduino 5 : tests unitaires | Pourquoi tester du code embarqué, et pourquoi sur le PC. Unity (`setUp`, `TEST_ASSERT_*`, `RUN_TEST`), une suite par dossier de `test/`, l'environnement `native`. Lecture détaillée d'un test, `pio test -e native`, lire un échec. Écrire un test (cas limite) | +| Arduino 6 : qualité et intégration continue | cppcheck (`pio check`, `check_flags`, suppressions en ligne justifiées). GitHub Actions : `ci.yml` décortiqué (déclencheurs, `permissions`, job `discover`, matrice, cache, version de PlatformIO figée, `credentials.h` factice, `--fail-on-defect=low`). Lire un échec dans l'onglet Actions. Badge, protection de branche en « Pour aller plus loin » | + +Chaque chapitre suit le template du projet : objectifs, durée, sections, +`L'essentiel`, `Pour aller plus loin`, 3 à 6 exercices avec leurs +solutions. Le calibrage du template (150 à 250 lignes de théorie) est +appliqué avec souplesse : les chapitres 4 et 6 citent du code et seront +plus longs. Chaque concept n'est enseigné qu'une fois, avec des renvois +« (voir chapitre Arduino N) ». + +## Exercices sans carte (exemples) + +- Créer un projet PlatformIO vide, ajouter une bibliothèque dans + `lib_deps`, compiler pour `seeed_xiao_esp32s3`. +- Ajouter un test à `test_reading` (85 °C après une lecture à 80 °C), + puis casser volontairement `isValidReading` et observer l'échec. +- Introduire un défaut repéré par cppcheck, puis le corriger. +- Sur un fork, pousser une branche qui casse un test et lire le résultat + dans l'onglet Actions. + +## Vérification + +- `./pdf/build` passe : PDF générés, tests pytest verts, aucun + `Missing character`. +- Les commandes et extraits cités sont vérifiés sur le dépôt `arduino` au + commit `1af068f` : `pio test -e native` (24 tests), `pio run -e xiao`, + `pio check`. +- Aucun caractère interdit : pas de dessin de boîtes, pas de flèches + Unicode, pas d'emojis. From b3e63211c63dc2443e23d9499b41539ca27fc47d Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 21:41:16 +0200 Subject: [PATCH 02/12] =?UTF-8?q?docs:=20plan=20d'impl=C3=A9mentation=20du?= =?UTF-8?q?=20module=20additionnel=20Arduino?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-15-module-arduino.md | 371 ++++++++++++++++++ .../2026-10-15-module-arduino.md.tasks.json | 14 + 2 files changed, 385 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-15-module-arduino.md create mode 100644 docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json diff --git a/docs/superpowers/plans/2026-10-15-module-arduino.md b/docs/superpowers/plans/2026-10-15-module-arduino.md new file mode 100644 index 0000000..a198663 --- /dev/null +++ b/docs/superpowers/plans/2026-10-15-module-arduino.md @@ -0,0 +1,371 @@ +# Module additionnel Arduino Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. + +**Goal:** Rédiger le module additionnel Arduino (6 chapitres) basé sur le projet +`DS18B20_MQTT`, et l'intégrer à la chaîne PDF. + +**Architecture:** Six fichiers Markdown dans +`supports/modules_additionnels/module_arduino/`, un PDF +`module_additionnel_arduino.pdf` déclaré dans `pdf/documents.yaml`. Le code +n'est pas copié : il est cité depuis https://github.com/pguinet/arduino, au +commit figé `1af068f` (dossier `sketches/XIAO-ESP32-S3/DS18B20_MQTT`). + +**Tech Stack:** Markdown (pandoc, LuaLaTeX via `./pdf/build`), PlatformIO 6.1.19 +(`~/.platformio/penv/bin/pio`) pour vérifier les commandes citées. + +**Spécification:** `docs/superpowers/specs/2026-10-15-module-arduino-design.md` +(à lire en entier avant toute tâche). + +--- + +## Règles communes à toutes les tâches de rédaction + +- **Source du code** : `~/github/arduino/sketches/XIAO-ESP32-S3/DS18B20_MQTT` + (lire les fichiers réels, ne jamais inventer un extrait). CI : + `~/github/arduino/.github/workflows/ci.yml`. Liens publics de la forme + `https://github.com/pguinet/arduino/blob/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT/`. +- **Template** : celui de `CLAUDE.md` (titre `# Arduino N — Titre`, bloc + `> **Objectifs**` / `> **Durée de lecture**`, sections, `## L'essentiel`, + `## Pour aller plus loin`, `## Exercices`, `### Solutions`). Exactement un + titre `#` par fichier. Modèle de style : `supports/modules_additionnels/module_git_windows/01_concepts_git.md`. +- **Public** : connaît Arduino par l'IDE officiel et les `.ino`, pas + PlatformIO, pas les tests, pas la CI. Lecture personnelle, hors séance : + vouvoiement, ton de cours, pas de référence au CID ni à VirtualBox. +- **Sans carte** : chaque exercice indique s'il demande la carte ; la majorité + doit s'en passer. +- **Caractères** : accents préservés ; pas de dessin de boîtes, pas de flèches + Unicode (`->`), pas d'emojis, pas de `°` hors texte courant si la police + le refuse (vérifié par `./pdf/build`). Les commentaires du code cité sont en + ASCII dans le dépôt source : les citer tels quels. +- **Non-répétition** : chaque notion n'est expliquée qu'une fois ; ailleurs, + `(voir chapitre Arduino N)`. Git/GitHub : `(voir module additionnel Git)`. +- **Vérification d'une commande** : toute commande `pio` citée avec sa sortie + est rejouée dans le projet source et la sortie recopiée réellement. +- **Longueur** : théorie 150 à 250 lignes (chap. 4 et 6 jusqu'à ~300 car ils + citent du code), `Pour aller plus loin` ~100 lignes max, 3 à 6 exercices. + +--- + +### Task 0: Catalogue PDF et squelettes + +**Files:** +- Modify: `pdf/documents.yaml` (après l'entrée `module_additionnel_docker.pdf`) +- Modify: `pdf/tests/test_documents.py` (`ATTENDUS`, après `"module_additionnel_docker.pdf"`) +- Create: les 6 fichiers de `supports/modules_additionnels/module_arduino/` + +**Step 1: Ajouter le nom attendu (test qui échoue)** + +Dans `ATTENDUS`, insérer `"module_additionnel_arduino.pdf",` juste après +`"module_additionnel_docker.pdf",`. + +**Step 2: Lancer les tests** + +Run: `./pdf/build test` +Expected: FAIL sur `test_catalogue...` (liste différente). + +**Step 3: Déclarer le document et créer les squelettes** + +Dans `pdf/documents.yaml`, après le bloc Docker (même indentation) : + +```yaml + - fichier: module_additionnel_arduino.pdf + titre: "Arduino : du sketch à l'intégration continue" + sous_titre: Module additionnel + sources: + - supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md + - supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md + - supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md + - supports/modules_additionnels/module_arduino/04_lecture_code.md + - supports/modules_additionnels/module_arduino/05_tests_unitaires.md + - supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md +``` + +Chaque squelette contient seulement son titre `#` et le bloc objectifs : +`# Arduino 1 — De l'Arduino à la XIAO ESP32-S3`, +`# Arduino 2 — De l'IDE Arduino à PlatformIO`, +`# Arduino 3 — MQTT et Home Assistant`, +`# Arduino 4 — Lecture du code`, +`# Arduino 5 — Les tests unitaires`, +`# Arduino 6 — Qualité et intégration continue`. + +**Step 4: Lancer tests et PDF** + +Run: `./pdf/build test && ./pdf/build pdf module_additionnel_arduino.pdf` +Expected: PASS, `build/pdf/module_additionnel_arduino.pdf` existe. + +**Step 5: Commit** + +```bash +git add pdf/ supports/modules_additionnels/module_arduino/ +git commit -m "feat: squelette du module additionnel Arduino" +``` + +--- + +### Task 1: Chapitre Arduino 1 — De l'Arduino à la XIAO ESP32-S3 + +**Files:** Modify: `supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md` + +**Contenu attendu (sections):** +1. Introduction du module : à qui il s'adresse, ce qu'on va construire + (sonde -> carte -> WiFi -> broker MQTT -> Home Assistant, en ASCII), ce + qu'on peut faire **sans carte** (tests, compilation, cppcheck, CI) et ce qui + demande la carte (flasher, moniteur, MQTT réel). Prérequis : IDE Arduino, + module Git. Lien vers le dépôt au commit `1af068f`. +2. Arduino, c'est trois choses : des cartes, un framework (`setup`/`loop`, + `digitalWrite`, `Serial`...), un IDE. Le framework existe sur bien d'autres + puces que l'AVR (ESP32 via le « core » d'Espressif). +3. L'ESP32-S3 : double cœur, WiFi + Bluetooth LE, USB natif ; comparaison + rapide avec un Uno (tableau : fréquence, RAM, flash, WiFi, tension 3,3 V). +4. Seeed Studio et la XIAO ESP32-S3 : format timbre-poste, broches `D0`..., + antenne U.FL **obligatoire** (symptôme `WiFi toujours absent (statut 1)`), + USB natif : port `/dev/ttyACM0` sous Linux, `COMx` sous Windows, + identifiant `303a:1001` commun à toutes les ESP32-S3. +5. La sonde DS18B20 et le bus 1-Wire : trois fils, adresse unique, résolution + 12 bits (750 ms), valeurs spéciales -127 et 85 (annoncer, détaillées au + chapitre 4). Résistance de tirage : pull-up interne ~45 kΩ, 4,7 kΩ externe + pour un câble long. Tableau de câblage du README et schéma ASCII. +6. Aperçu du projet : arborescence du dossier (sortie de `find` hors `.pio`, + commentée), rôle de chaque partie, plan des chapitres suivants. + +**Exercices (3-4)** : identifier le port d'une carte branchée (`ls /dev/ttyACM*`, +`lsusb`), carte avec carte ; lire le tableau de câblage et dire ce qui se passe +sans pull-up ; associer chaque fichier du projet à son rôle ; cloner le dépôt +et se placer au commit `1af068f` (`git switch --detach 1af068f`). + +**Verify:** `./pdf/build pdf module_additionnel_arduino.pdf` passe ; relire +le chapitre contre la checklist des règles communes. + +**Commit:** `git commit -m "docs: chapitre Arduino 1 (Arduino, XIAO ESP32-S3, DS18B20)"` + +--- + +### Task 2: Chapitre Arduino 2 — De l'IDE Arduino à PlatformIO + +**Files:** Modify: `.../02_ide_vers_platformio.md` + +**Contenu attendu:** +1. Ce que l'IDE fait sans le dire : un `.ino` est concaténé en `.cpp`, + `#include ` ajouté, prototypes générés (montrer un `.ino` qui + compile et le même fichier `.cpp` qui échoue faute de prototype), bibliothèques + installées globalement sans version, carte choisie dans un menu (rien dans + le projet). Conséquence : projet non reproductible sur une autre machine ni + en CI. +2. PlatformIO : outil en ligne de commande (`pio`) + extension VS Code ; il + télécharge lui-même plateformes, chaînes de compilation et bibliothèques. + Installation : extension VS Code (recommandée) ou `pipx install platformio`. + Sous Linux, règles udev (`99-platformio-udev.rules`) et groupe `dialout` + (renvoi au module 5 du cours pour les groupes). +3. Arborescence d'un projet : `platformio.ini`, `src/`, `lib/`, `include/`, + `test/`, `.pio/` (à ignorer dans Git). +4. `platformio.ini` du projet cité en entier puis commenté ligne par ligne : + `[platformio] default_envs`, `[env:xiao]` (`platform` pioarduino figée par + URL de version, `board`, `framework`, ports, `monitor_speed`, `lib_deps` avec + `^` et sens du caret), `check_*` (annoncé, détaillé chap. 6), + `[env:native]` (annoncé, détaillé chap. 5). +5. Les commandes : `pio run -e xiao`, `pio run -e xiao -t upload`, + `pio device monitor`, `pio run -t clean`, `pio pkg list`. Équivalents + boutons VS Code. Sortie réelle de `pio run -e xiao` (tableau RAM/Flash) + recopiée après exécution dans le projet source (avec `src/credentials.h` + présent). +6. Les bibliothèques : `lib_deps` (registre PlatformIO, `auteur/nom@version`) + et `lib/` (bibliothèques locales du projet : `temp_logic`). + +**Exercices** : créer un projet vide `pio project init --board seeed_xiao_esp32s3` +et y porter un « Blink » (prototype à ajouter) ; ajouter `ArduinoJson` en +`lib_deps` et compiler ; expliquer `^2.3.8` (quelles versions acceptées) ; +lister les environnements du projet et ce que fait `pio run` sans `-e`. + +**Verify:** rejouer chaque commande citée (sans upload) ; `./pdf/build pdf module_additionnel_arduino.pdf`. + +**Commit:** `git commit -m "docs: chapitre Arduino 2 (de l'IDE à PlatformIO)"` + +--- + +### Task 3: Chapitre Arduino 3 — MQTT et Home Assistant + +**Files:** Modify: `.../03_mqtt_home_assistant.md` + +**Contenu attendu:** +1. Le besoin : un capteur publie, plusieurs consommateurs lisent, sans se + connaître. Modèle publication/abonnement, schéma ASCII broker au centre. +2. Vocabulaire : broker (Mosquitto), client, sujet (`a/b/c`, jokers `+` et `#`), + message, QoS 0/1/2 en bref (le projet publie en QoS 0), session. +3. Messages retenus : le dernier message d'un sujet est remis à tout nouvel + abonné ; usage pour la découverte et la disponibilité, pas pour la mesure. +4. Testament (Last Will) : `status` passe à `offline` si la carte disparaît. + Citer l'appel `mqtt.connect(deviceId, MQTT_USER, MQTT_PASS, topics.status, 0, true, "offline")` + et le décoder argument par argument. +5. Home Assistant et MQTT Discovery : HA écoute `homeassistant/#` ; un message + retenu sur `homeassistant/sensor//temperature/config` crée le capteur. + Montrer le JSON produit par `buildDiscovery` (le reconstituer à partir de + `lib/temp_logic/ha_discovery.cpp` et `test/test_discovery/test_main.cpp`, + indenté), expliquer `availability`, `availability_mode: all`, + `device_class`, `unit_of_measurement`, `unique_id`. Redémarrage de HA : + `homeassistant/status` = `online` -> republication. +6. Tableau des sujets du projet (repris du README). +7. Encadré « Et en vrai ? » : broker et HA pourront tourner sur le futur + homelab du club (renvoi au module Docker). En attendant, `mosquitto_sub -v -t 'xiao_ds18b20/#'` + sur n'importe quel broker. + +**Exercices** : prédire ce que voit un abonné arrivant après coup sur chaque +sujet (retenu ou non) ; écrire le filtre pour toutes les températures de +toutes les sondes ; expliquer pourquoi la mesure n'est pas retenue ; (option +avec Docker) lancer `eclipse-mosquitto` et publier/lire à la main avec +`mosquitto_pub`/`mosquitto_sub` en simulant la carte. + +**Verify:** l'extrait JSON correspond exactement à ce que vérifie `test_discovery` ; +`./pdf/build pdf module_additionnel_arduino.pdf`. + +**Commit:** `git commit -m "docs: chapitre Arduino 3 (MQTT et Home Assistant)"` + +--- + +### Task 4: Chapitre Arduino 4 — Lecture du code + +**Files:** Modify: `.../04_lecture_code.md` + +**Contenu attendu:** +1. L'architecture : pourquoi deux couches. `src/main.cpp` dépend du matériel + (WiFi, OneWire, PubSubClient) ; `lib/temp_logic` n'inclut que + ``/`` (+ ArduinoJson, qui compile aussi sur PC) et se + teste sur PC. Schéma ASCII des dépendances. Le namespace `temp`. +2. Les secrets : `#include "credentials.h"`, fichier hors dépôt (`.gitignore`), + modèle `credentials.h.example`, garde-fous `#ifndef WIFI_SSID / #error`. +3. `setup()` : pull-up, `setWaitForConversion(false)`, MAC lue dans l'eFuse et + pourquoi (`WiFi.macAddress()` renvoie des zéros avant démarrage), + `makeDeviceId`, `WiFi.begin` non bloquant, réglages PubSubClient + (`setBufferSize(1024)` car la découverte dépasse 256 octets). +4. La boucle non bloquante : `loop()` = trois `handle*()` + `delay(10)`. Le + motif `if (millis() - last < INTERVAL) return;`, et pourquoi la soustraction + non signée survit au débordement de `millis()` (~49,7 jours ; exemple + chiffré). Comparer avec un sketch « IDE » à base de `delay(10000)`. +5. `handleSensor()` : machine à deux états (`converting`), conversion + asynchrone 750 ms. `isValidReading` : -127, plage -55..125, 85 sauf si la + précédente > 75 (citer l'implémentation de `temp_logic.cpp`). +6. `PublishPolicy` (écart >= 0,2 °C ou 5 min, `reset()` à la reconnexion) et + `ProbeHealth` (3 échecs -> hors ligne) : interface du `.h` citée, choix + « dixièmes entiers » (`toTenths`) pour éviter de comparer des `float`. +7. WiFi et MQTT : reconnexion du core + relance toutes les 30 s ; MQTT réessayé + 5 s après la fin de la tentative précédente (`lastAttempt = 0 - RETRY_INTERVAL_MS` + pour une première tentative immédiate) ; callback `onMqttMessage`. + +**Exercices** : transformer un sketch bloquant (`delay`) donné en version +`millis()` ; calculer `millis() - last` juste après le débordement (valeurs +données) ; dire ce que fait `isValidReading` pour une liste d'entrées ; +justifier pourquoi `PublishPolicy` est dans `lib/` et `handleWifi` dans `src/`. + +**Verify:** chaque extrait cité est identique au fichier au commit `1af068f` +(`git -C ~/github/arduino show 1af068f:sketches/XIAO-ESP32-S3/DS18B20_MQTT/`) ; +`./pdf/build pdf module_additionnel_arduino.pdf`. + +**Commit:** `git commit -m "docs: chapitre Arduino 4 (lecture du code)"` + +--- + +### Task 5: Chapitre Arduino 5 — Les tests unitaires + +**Files:** Modify: `.../05_tests_unitaires.md` + +**Contenu attendu:** +1. Pourquoi tester : bugs d'arrondi, cas limites (85 °C, débordement), régressions ; + tester sur la carte est lent et ne couvre pas les cas rares. +2. Tester sur le PC : `[env:native]` (`platform = native`, `-std=c++17 -Wall -Wextra`, + ArduinoJson seul). Possible grâce à la séparation du chapitre 4. Ce qu'on ne + teste pas ainsi (le matériel) et ce qu'on y gagne. +3. Unity : `#include `, `setUp`/`tearDown`, `TEST_ASSERT_TRUE`, + `TEST_ASSERT_EQUAL_INT`, `TEST_ASSERT_EQUAL_STRING`, `UNITY_BEGIN`, + `RUN_TEST`, `UNITY_END`. Une suite = un dossier `test/test_xxx/`. +4. Lecture détaillée de `test/test_reading/test_main.cpp` (citer en entier ou + l'essentiel), noms de tests comme documentation. +5. Lancer : `pio test -e native` (sortie réelle, 24 tests), `-f test_reading`, + `-v`. Lire un échec : casser `toTenths` (ex. troncature au lieu d'arrondi), + recopier la sortie réelle de l'échec, puis annuler (`git restore`). +6. Écrire un test : démarche (cas nominal, limites, erreurs), exemple guidé + d'ajout dans `test_publish` (heartbeat exactement à 300000 ms). + +**Exercices** : ajouter un test 85 °C après 80 °C (accepté) et après 20 °C +(rejeté) ; ajouter un test `formatTenths(-5)` -> `"-0.5"` ; casser +volontairement `ProbeHealth::record` et identifier le test qui échoue ; +proposer un cas non couvert par les 24 tests actuels. + +**Verify:** chaque test d'exercice compilé et exécuté dans une copie de travail +du dépôt `arduino` (puis annulé) ; sorties recopiées réelles ; +`./pdf/build pdf module_additionnel_arduino.pdf`. + +**Commit:** `git commit -m "docs: chapitre Arduino 5 (tests unitaires)"` + +--- + +### Task 6: Chapitre Arduino 6 — Qualité et intégration continue + +**Files:** Modify: `.../06_qualite_integration_continue.md` + +**Contenu attendu:** +1. L'analyse statique : ce que voit cppcheck sans exécuter le code. `check_tool`, + `check_flags` (`--enable=warning,style,performance`, `--inline-suppr`, + exclusion de `.pio`), `check_src_filters`. `pio check -e xiao --skip-packages` + (sortie réelle). Suppression en ligne justifiée : `constParameterCallback` + dans `main.cpp`. +2. L'intégration continue : principe (chaque push vérifié sur une machine + neuve), GitHub Actions : workflow, job, step, runner, action (`uses:`). +3. `ci.yml` cité en entier puis décortiqué : `on` (push sur `main`, PR), + `permissions: contents: read`, job `discover` (grep des `[env:native]`, + `jq`, `outputs`), job `platformio` (`needs`, `matrix` dynamique via + `fromJSON`, `fail-fast: false`, `working-directory`), cache `~/.platformio` + indexé sur `platformio.ini`, PlatformIO figé `==6.1.19`, `credentials.h` + factice, tests, détection de l'environnement par `configparser`, + compilation, cppcheck `--fail-on-defect=low`. Ne pas détailler le job + `claude-dash-agent` (une phrase : autre projet du dépôt). +4. Lire un résultat : onglet Actions, statut sur la PR, logs d'un step. +5. Synthèse : la chaîne complète du développeur (éditer -> `pio test` -> + `pio check` -> commit -> push -> CI -> fusion). + +**Pour aller plus loin** : badge de statut dans le README, protection de +branche exigeant la CI, `act` pour lancer un workflow en local, tests sur +cible (`pio test -e xiao`), Dependabot pour les versions d'actions. + +**Exercices** : introduire un défaut cppcheck (variable inutilisée, +paramètre passé par valeur) et le faire signaler ; dire quel step échoue si +`credentials.h.example` perd `MQTT_SERVER` ; sur un fork, pousser une branche +qui casse un test et lire le log ; adapter `ci.yml` pour lancer aussi les tests +sur `pull_request` vers une autre branche. + +**Verify:** sortie cppcheck réelle ; extrait `ci.yml` identique au commit +`1af068f` ; `./pdf/build pdf module_additionnel_arduino.pdf`. + +**Commit:** `git commit -m "docs: chapitre Arduino 6 (qualité et intégration continue)"` + +--- + +### Task 7: Documentation du dépôt et vérification finale + +**Files:** +- Modify: `CLAUDE.md` (section « Modules additionnels » : bloc Arduino avec ses + 6 chapitres, prérequis « IDE Arduino ; module additionnel Git », durée + « 6-8 heures de lecture personnelle, hors séances » ; arborescence : + `module_arduino/` et ses 6 fichiers ; remarque finale : liste des modules) +- Modify: `README.md` (liste des modules / PDF, au même format que Docker) + +**Step 1:** Mettre à jour les deux fichiers. + +**Step 2:** Vérification complète + +Run: `./pdf/build && ./pdf/build check` +Expected: tous les PDF générés, pytest vert, ruff et shellcheck sans erreur. + +**Step 3:** Relecture : grep des caractères interdits + +Run: `grep -nP '[\x{2190}-\x{21FF}\x{2500}-\x{257F}\x{1F300}-\x{1FAFF}]' supports/modules_additionnels/module_arduino/*.md` +Expected: aucune sortie. + +**Step 4:** Commit + +```bash +git add CLAUDE.md README.md +git commit -m "docs: module additionnel Arduino dans le plan et le README" +``` + +**Step 5:** Revue de code (superpowers-extended-cc:requesting-code-review), +puis PR vers `master`. diff --git a/docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json b/docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json new file mode 100644 index 0000000..b31eaf5 --- /dev/null +++ b/docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json @@ -0,0 +1,14 @@ +{ + "planPath": "docs/superpowers/plans/2026-10-15-module-arduino.md", + "tasks": [ + {"id": 0, "subject": "Task 0: Catalogue PDF et squelettes", "status": "pending"}, + {"id": 1, "subject": "Task 1: Chapitre Arduino 1", "status": "pending", "blockedBy": [0]}, + {"id": 2, "subject": "Task 2: Chapitre Arduino 2", "status": "pending", "blockedBy": [0]}, + {"id": 3, "subject": "Task 3: Chapitre Arduino 3", "status": "pending", "blockedBy": [0]}, + {"id": 4, "subject": "Task 4: Chapitre Arduino 4", "status": "pending", "blockedBy": [0]}, + {"id": 5, "subject": "Task 5: Chapitre Arduino 5", "status": "pending", "blockedBy": [0]}, + {"id": 6, "subject": "Task 6: Chapitre Arduino 6", "status": "pending", "blockedBy": [0]}, + {"id": 7, "subject": "Task 7: Documentation du dépôt et vérification finale", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6]} + ], + "lastUpdated": "2026-10-15T00:00:00Z" +} From 923928c501357e378ccce2591c843fbb92931c02 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 21:48:00 +0200 Subject: [PATCH 03/12] feat: squelette du module additionnel Arduino Co-Authored-By: Claude Opus 5.5 --- pdf/documents.yaml | 10 ++++++++++ pdf/tests/test_documents.py | 3 ++- .../module_arduino/01_arduino_xiao_esp32s3.md | 5 +++++ .../module_arduino/02_ide_vers_platformio.md | 5 +++++ .../module_arduino/03_mqtt_home_assistant.md | 5 +++++ .../module_arduino/04_lecture_code.md | 5 +++++ .../module_arduino/05_tests_unitaires.md | 5 +++++ .../module_arduino/06_qualite_integration_continue.md | 5 +++++ 8 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md create mode 100644 supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md create mode 100644 supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md create mode 100644 supports/modules_additionnels/module_arduino/04_lecture_code.md create mode 100644 supports/modules_additionnels/module_arduino/05_tests_unitaires.md create mode 100644 supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md diff --git a/pdf/documents.yaml b/pdf/documents.yaml index ac77ed2..09b81b4 100644 --- a/pdf/documents.yaml +++ b/pdf/documents.yaml @@ -85,6 +85,16 @@ documents: - travaux_pratiques/tp_additionnels/tp_docker/tp3_volumes_donnees.md - travaux_pratiques/tp_additionnels/tp_docker/tp4_reseaux_communication.md - travaux_pratiques/tp_additionnels/tp_docker/tp5_compose_orchestration.md + - fichier: module_additionnel_arduino.pdf + titre: "Arduino : du sketch à l'intégration continue" + sous_titre: Module additionnel + sources: + - supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md + - supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md + - supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md + - supports/modules_additionnels/module_arduino/04_lecture_code.md + - supports/modules_additionnels/module_arduino/05_tests_unitaires.md + - supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md - fichier: annexe_installation_ssh.pdf titre: Se connecter à sa VM distante (SSH) sous_titre: Annexe diff --git a/pdf/tests/test_documents.py b/pdf/tests/test_documents.py index e1dadc3..ed2515e 100644 --- a/pdf/tests/test_documents.py +++ b/pdf/tests/test_documents.py @@ -83,6 +83,7 @@ def test_selectionne_les_documents_demandes(): "module_additionnel_git.pdf", "module_additionnel_git_windows.pdf", "module_additionnel_docker.pdf", + "module_additionnel_arduino.pdf", "annexe_installation_ssh.pdf", "annexe_installation_virtualbox.pdf", ] @@ -92,7 +93,7 @@ def catalogue_du_depot(): return generer.charger_documents(generer.DOSSIER_PDF / "documents.yaml", generer.RACINE) -def test_le_catalogue_du_depot_declare_les_13_pdf(): +def test_le_catalogue_du_depot_declare_les_14_pdf(): assert [document.fichier for document in catalogue_du_depot().documents] == ATTENDUS diff --git a/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md new file mode 100644 index 0000000..ead261d --- /dev/null +++ b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md @@ -0,0 +1,5 @@ +# Arduino 1 — De l'Arduino à la XIAO ESP32-S3 + +> **Objectifs** : à rédiger. +> +> **Durée de lecture** : à préciser. diff --git a/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md new file mode 100644 index 0000000..a4a9836 --- /dev/null +++ b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md @@ -0,0 +1,5 @@ +# Arduino 2 — De l'IDE Arduino à PlatformIO + +> **Objectifs** : à rédiger. +> +> **Durée de lecture** : à préciser. diff --git a/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md new file mode 100644 index 0000000..dd19d9f --- /dev/null +++ b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md @@ -0,0 +1,5 @@ +# Arduino 3 — MQTT et Home Assistant + +> **Objectifs** : à rédiger. +> +> **Durée de lecture** : à préciser. diff --git a/supports/modules_additionnels/module_arduino/04_lecture_code.md b/supports/modules_additionnels/module_arduino/04_lecture_code.md new file mode 100644 index 0000000..dcc5809 --- /dev/null +++ b/supports/modules_additionnels/module_arduino/04_lecture_code.md @@ -0,0 +1,5 @@ +# Arduino 4 — Lecture du code + +> **Objectifs** : à rédiger. +> +> **Durée de lecture** : à préciser. diff --git a/supports/modules_additionnels/module_arduino/05_tests_unitaires.md b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md new file mode 100644 index 0000000..245d4c6 --- /dev/null +++ b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md @@ -0,0 +1,5 @@ +# Arduino 5 — Les tests unitaires + +> **Objectifs** : à rédiger. +> +> **Durée de lecture** : à préciser. diff --git a/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md new file mode 100644 index 0000000..eae60a6 --- /dev/null +++ b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md @@ -0,0 +1,5 @@ +# Arduino 6 — Qualité et intégration continue + +> **Objectifs** : à rédiger. +> +> **Durée de lecture** : à préciser. From f8d57f1d07893999f5735c1357d9948ee1dd4d0d Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:13:39 +0200 Subject: [PATCH 04/12] docs: chapitre Arduino 1 (Arduino, XIAO ESP32-S3, DS18B20) Co-Authored-By: Claude Opus 5.5 --- .../module_arduino/01_arduino_xiao_esp32s3.md | 425 +++++++++++++++++- 1 file changed, 423 insertions(+), 2 deletions(-) diff --git a/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md index ead261d..9513376 100644 --- a/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md +++ b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md @@ -1,5 +1,426 @@ # Arduino 1 — De l'Arduino à la XIAO ESP32-S3 -> **Objectifs** : à rédiger. +> **Objectifs** : à la fin de ce chapitre, vous saurez : +> - distinguer les trois sens du mot « Arduino » : les cartes, le framework et l'IDE ; +> - situer l'ESP32-S3 et la Seeed Studio XIAO ESP32-S3 par rapport à un Arduino Uno ; +> - expliquer le rôle de l'antenne U.FL et de l'USB natif de la XIAO ; +> - décrire la sonde DS18B20, le bus 1-Wire et le rôle de la résistance de tirage ; +> - vous repérer dans l'arborescence du projet étudié dans ce module. > -> **Durée de lecture** : à préciser. +> **Durée de lecture** : environ 45 min. + +--- + +Ce module s'adresse aux lecteurs qui connaissent Arduino par l'IDE +officiel et les fichiers `.ino`. Il montre comment travailler sur un +microcontrôleur avec les pratiques de l'industrie : construction +reproductible avec PlatformIO, code découpé pour être testable, tests +unitaires, analyse statique et intégration continue. Ce premier +chapitre présente le module, puis le matériel du projet étudié. + +## Le module en bref + +### Ce que l'on étudie + +Tout le module s'appuie sur un projet réel et public, `DS18B20_MQTT` : +une sonde de température dont les mesures s'affichent dans Home +Assistant, un logiciel de domotique. + +``` +DS18B20 --1-Wire--> XIAO ESP32-S3 --WiFi--> broker MQTT --> Home Assistant +(sonde) (la carte) (serveur) (affichage) +``` + +La carte lit la sonde toutes les 10 secondes et envoie la mesure par +WiFi à un **broker MQTT**, un serveur qui reçoit les messages et les +redistribue ; Home Assistant les reçoit, affiche la mesure et en garde +l'historique. + +Le code se trouve dans le dépôt https://github.com/pguinet/arduino, +dossier `sketches/XIAO-ESP32-S3/DS18B20_MQTT`. Le module cite toujours +le même commit, `1af068f`, pour que le texte et le code restent +d'accord : +https://github.com/pguinet/arduino/tree/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT + +### Prérequis et organisation + +- avoir déjà écrit et téléversé un sketch avec l'IDE Arduino ; +- avoir lu le module additionnel Git : cloner, commit, branche, GitHub + ne sont pas réexpliqués (voir module additionnel Git). + +Le module se lit seul, sur votre propre PC, en 6 à 8 heures. Linux sert +d'exemple ; les différences avec Windows et macOS sont signalées. + +Vous n'avez **pas besoin de la carte** pour l'essentiel du module, et +chaque exercice indique s'il la demande : + +| Sans carte, sur le PC seul | Avec la carte (et un broker) | +|----------------------------|------------------------------| +| compiler le projet | téléverser le programme (flasher) | +| lancer les tests unitaires | lire le moniteur série | +| lancer l'analyse statique (cppcheck) | voir les messages MQTT réels | +| faire tourner l'intégration continue | voir le capteur dans Home Assistant | + +| Chapitre | Contenu | +|----------|---------| +| Arduino 1 | Le matériel : ESP32-S3, XIAO, DS18B20, bus 1-Wire, le projet | +| Arduino 2 | De l'IDE Arduino à PlatformIO : `platformio.ini`, `pio run` | +| Arduino 3 | MQTT et Home Assistant : sujets, messages retenus, découverte | +| Arduino 4 | Lecture du code : architecture, boucle non bloquante, reconnexions | +| Arduino 5 | Les tests unitaires : Unity, `pio test`, écrire un test | +| Arduino 6 | Qualité et intégration continue : cppcheck, GitHub Actions | + +## Arduino, c'est trois choses + +Le mot « Arduino » désigne trois choses, que l'IDE présente comme un +tout : + +- **des cartes** : l'Uno et ses cousines, dont les schémas sont publiés + sous licence libre, d'où les nombreux clones ; +- **un framework** : `setup()` et `loop()`, `pinMode()`, + `digitalWrite()`, `millis()`, l'objet `Serial`... Il rend un programme + lisible sans connaître les registres du microcontrôleur ; +- **un IDE** : l'éditeur qui compile et téléverse le sketch. + +Le framework n'est pas lié aux cartes Arduino. Pour chaque famille de +puces, une implémentation appelée **core** traduit `digitalWrite()` ou +`Serial.println()` pour cette puce. Le core d'origine vise les AVR, +comme l'ATmega328P de l'Uno ; Espressif publie le sien, +`arduino-esp32`, pour ses ESP32. Dans ce module, c'est PlatformIO qui +l'installe (voir chapitre Arduino 2). Un sketch se compile donc pour un +Uno comme pour une ESP32, aux différences de matériel près : broches, +tension, mémoire. + +## L'ESP32-S3 + +L'**ESP32-S3** est un microcontrôleur de la société chinoise Espressif +Systems, annoncé fin 2020. Il réunit sur une seule puce deux cœurs +Xtensa LX7 de 32 bits à 240 MHz, 512 Ko de SRAM, le **WiFi** 2,4 GHz +(802.11 b/g/n), le **Bluetooth 5 LE** et un contrôleur **USB** intégré. +Comparé à l'Arduino Uno R3 : + +| | Arduino Uno R3 | XIAO ESP32-S3 | +|---|---|---| +| Microcontrôleur | ATmega328P (AVR, 8 bits) | ESP32-S3 (Xtensa LX7, 32 bits) | +| Cœurs, fréquence | 1 à 16 MHz | 2 à 240 MHz | +| RAM | 2 Ko | 512 Ko de SRAM + 8 Mo de PSRAM | +| Flash (programme) | 32 Ko | 8 Mo | +| WiFi, Bluetooth | non | WiFi 2,4 GHz, Bluetooth 5 LE | +| Tension des broches | 5 V | 3,3 V | +| USB | via une seconde puce (ATmega16U2) | natif | +| Dimensions | 68,6 x 53,4 mm | 21 x 17,8 mm | + +Deux conséquences pratiques. La RAM n'est plus le souci quotidien +qu'elle est sur un Uno : on construit sans peine un message JSON de +plusieurs centaines d'octets. Et les broches travaillent en **3,3 V** : +un module prévu pour du 5 V peut endommager la carte. La sonde DS18B20, +elle, accepte de 3,0 à 5,5 V. + +## Seeed Studio et la XIAO ESP32-S3 + +**Seeed Studio** est un fabricant chinois de cartes de développement. Sa +gamme **XIAO** décline un même format de la taille d'un timbre pour +plusieurs microcontrôleurs (RP2040, ESP32-C3, ESP32-C6, ESP32-S3...), +avec un connecteur USB-C et des broches nommées `D0` à `D10`. La XIAO +ESP32-S3 existe en version de base et en version *Sense* (caméra et +lecteur microSD) ; le projet utilise la version de base. Sur cette +carte, la constante `D0` du framework désigne la broche GPIO1 de la +puce. + +### L'antenne est obligatoire + +La XIAO ESP32-S3 **n'a pas d'antenne intégrée**. Elle est livrée avec une +antenne souple qui se clipse sur un minuscule connecteur **U.FL**. Sans +elle, la carte ne voit pas le réseau, même à un mètre de la box. Le +programme du projet l'affiche alors sur le moniteur série : + +``` +WiFi toujours absent (statut 1) +``` + +Le statut 1 est `WL_NO_SSID_AVAIL` : aucun réseau de ce nom n'est +visible. Vérifiez l'antenne avant de soupçonner le mot de passe. + +### L'USB natif + +Sur un Uno, le microcontrôleur ne parle pas USB : une seconde puce +traduit entre l'USB du PC et sa liaison série. L'ESP32-S3 contient son +propre contrôleur USB : c'est lui que le PC voit. La carte apparaît +comme un port série : `/dev/ttyACM0` sous Linux (le numéro augmente avec +plusieurs cartes), `COM3`, `COM4`... sous Windows (gestionnaire de +périphériques), `/dev/cu.usbmodem...` sous macOS. + +La commande `lsusb` affiche l'identifiant USB `fabricant:produit` de la +carte, `303a:1001` : `303a` désigne Espressif, `1001` le contrôleur USB +de la puce. Cet identifiant est donc **le même pour toutes les cartes +ESP32-S3**, quel que soit leur fabricant. Pour vérifier que c'est bien +la XIAO qui est branchée, le README du projet lit la taille de sa flash +(8 Mo) avec l'outil `esptool`. + +## La sonde DS18B20 et le bus 1-Wire + +La **DS18B20** (Maxim Integrated, aujourd'hui Analog Devices) est un +thermomètre numérique. Le projet utilise sa version étanche : la puce +est enfermée dans un tube en inox, au bout d'un câble à trois fils. + +| Caractéristique | Valeur | +|-----------------|--------| +| Plage de mesure | -55 °C à +125 °C | +| Précision | 0,5 °C entre -10 °C et +85 °C | +| Résolution | 9 à 12 bits (12 bits : 0,0625 °C) | +| Durée de conversion | 750 ms au plus en 12 bits | +| Alimentation | 3,0 V à 5,5 V | + +Elle communique par le bus **1-Wire** : un seul fil de données +(`DATA`), plus la masse et l'alimentation. Chaque sonde porte une +**adresse unique** de 64 bits gravée en usine : plusieurs sondes peuvent +partager le même fil, et le programme les compte au démarrage +(`Sondes detectees : 1`). Une mesure se fait en deux temps : demander +une **conversion**, puis lire le résultat 750 ms plus tard (voir +chapitre Arduino 4 pour l'attente non bloquante). + +Deux valeurs sont suspectes, et seront traitées au chapitre Arduino 4 : +**-127**, renvoyée par la bibliothèque `DallasTemperature` quand la +sonde ne répond pas (fil coupé, soudure ratée), et **85**, contenue par +la sonde à la mise sous tension, avant toute conversion. + +### La résistance de tirage + +Sur le fil `DATA`, la sonde et la carte ne font que **tirer la ligne vers +0 V** pour envoyer leurs bits. Au repos, une **résistance de tirage** +(*pull-up*), placée entre `DATA` et l'alimentation, ramène la ligne à +3,3 V. Sans elle, la ligne flotte et les lectures échouent (-127). + +Les montages classiques utilisent une résistance externe de 4,7 +kilohms. Le projet s'en passe : il active la résistance de tirage +**interne** de l'ESP32-S3, d'environ 45 kilohms, avant de démarrer le +bus. C'est cette ligne de `setup()`, dans `src/main.cpp` : + +```cpp + pinMode(ONE_WIRE_PIN, INPUT_PULLUP); +``` + +Cette résistance, dix fois plus forte, suffit pour le mètre de câble +d'une sonde étanche, pas pour plusieurs mètres. En cas de lectures à +-127, on soude une 4,7 kilohms entre `DATA` et `3V3`, sans toucher au +code. Le câblage, repris du README : + +| Fil de la sonde | XIAO ESP32S3 | +|---|---| +| Rouge (VDD) | 3V3 | +| Noir (GND) | GND | +| Jaune (DATA) | D0 (GPIO1) | + +``` + XIAO ESP32-S3 sonde DS18B20 + 3V3 o------------+---------------------- rouge (VDD) + | + [45 k] pull-up interne (INPUT_PULLUP) + | + D0 o------------+---------------------- jaune (DATA) + GND o----------------------------------- noir (GND) +``` + +Schéma électrique : sur la carte, les broches sont disposées autrement. + +## Le projet DS18B20_MQTT + +Dans un clone du dépôt, le dossier du projet contient ces fichiers +(sortie de `find . -type f | sort`, avant toute compilation) : + +``` +./case/ATTRIBUTION.md +./case/seeed-studio-xiao-esp32-c6-base-plain.stl +./case/seeed-studio-xiao-esp32-c6-lid-x.stl +./lib/temp_logic/ha_discovery.cpp +./lib/temp_logic/ha_discovery.h +./lib/temp_logic/temp_logic.cpp +./lib/temp_logic/temp_logic.h +./platformio.ini +./README.md +./src/main.cpp +./test/test_discovery/test_main.cpp +./test/test_probe/test_main.cpp +./test/test_publish/test_main.cpp +./test/test_reading/test_main.cpp +``` + +| Chemin | Rôle | Chapitre | +|--------|------|----------| +| `platformio.ini` | description du projet : carte, framework, bibliothèques | Arduino 2 | +| `src/main.cpp` | le programme : sonde, WiFi, MQTT (code lié au matériel) | Arduino 4 | +| `lib/temp_logic/temp_logic.*` | validation, arrondi, décision de publier, état de la sonde | Arduino 4 | +| `lib/temp_logic/ha_discovery.*` | identifiant, sujets MQTT, message de découverte | Arduino 3 et 4 | +| `test/` | tests unitaires, une suite par dossier | Arduino 5 | +| `case/` | boîtier imprimable en 3D (fichiers STL) | - | +| `README.md` | matériel, câblage, configuration, dépannage | - | + +Il n'y a pas de fichier `.ino` : le programme est un fichier C++ +ordinaire. Il manque aussi `src/credentials.h`, qui contient les +identifiants du WiFi et du broker : il est volontairement exclu du dépôt +(voir chapitre Arduino 4). + +## L'essentiel + +| Commande / Notion | Usage | +|-------------------|-------| +| Arduino | Trois choses : des cartes, un framework (`setup`, `loop`, `Serial`...), un IDE | +| Core | Implémentation du framework pour une famille de puces (`arduino-esp32` pour les ESP32) | +| ESP32-S3 | Microcontrôleur Espressif : 2 cœurs à 240 MHz, WiFi, Bluetooth 5 LE, USB natif | +| XIAO ESP32-S3 | Carte Seeed Studio de 21 x 17,8 mm, 8 Mo de flash, broches `D0` à `D10` | +| Antenne U.FL | Obligatoire sur la XIAO ; sans elle : `WiFi toujours absent (statut 1)` | +| USB natif | La puce gère l'USB elle-même ; port `/dev/ttyACM0` (Linux), `COMx` (Windows) | +| `303a:1001` | Identifiant USB commun à toutes les ESP32-S3 (`lsusb`) | +| DS18B20 | Sonde numérique, -55 à +125 °C, 750 ms par conversion en 12 bits | +| 1-Wire | Bus à un fil de données ; chaque sonde a une adresse unique de 64 bits | +| -127 et 85 | Sonde muette, et valeur de mise sous tension : lectures suspectes | +| Pull-up | Ramène `DATA` à 3,3 V au repos ; interne (~45 kilohms) ou externe (4,7 kilohms) | +| `INPUT_PULLUP` | Active la résistance de tirage interne d'une broche | + +## Pour aller plus loin + +**Les autres ESP32.** Espressif décline l'ESP32 en plusieurs familles. +L'ESP32 « classique » (2016) a deux cœurs Xtensa LX6 et pas d'USB natif. +Les ESP32-C3 et ESP32-C6 ont un cœur principal RISC-V ; le C6 ajoute le +WiFi 6 et les protocoles domotiques Thread et Zigbee. Le S3 se distingue par +ses deux cœurs, ses instructions vectorielles (utiles pour l'audio ou la +reconnaissance d'images) et son grand nombre de broches. Le boîtier du +dossier `case/` est prévu pour les XIAO C3, C6 et S3, qui ont le même +format. + +**ESP-IDF et FreeRTOS.** Sur l'ESP32, le framework +Arduino tourne au-dessus d'ESP-IDF, le kit de développement officiel +d'Espressif, et de FreeRTOS, un petit système temps réel. `loop()` est +en fait une tâche FreeRTOS, et le WiFi tourne dans d'autres tâches. On +peut appeler directement les fonctions d'ESP-IDF depuis un sketch : le +projet le fait avec `esp_read_mac()`, qui lit l'adresse MAC dans les +**eFuses** de la puce, une mémoire programmée une seule fois en usine +(voir chapitre Arduino 4). + +**Quand l'USB natif disparaît.** Avec une seconde puce USB, comme sur +l'Uno, le port série existe tant que la carte est alimentée. Avec l'USB +natif, c'est le programme qui le fait vivre : si le programme plante en +boucle ou met la puce en sommeil profond, le port `/dev/ttyACM0` +disparaît, et le téléversement échoue. La parade : débrancher la carte, +maintenir le bouton **BOOT** enfoncé, la rebrancher, puis relâcher le +bouton. La puce démarre alors sur son chargeur interne, qui expose +toujours le port USB, et accepte un nouveau programme. + +**ttyACM ou ttyUSB.** Sous Linux, le nom du port dépend du pilote, pas +de la carte. Un appareil qui se présente comme un modem USB standard +(classe CDC ACM), comme l'ESP32-S3 ou l'ATmega16U2 de l'Uno R3, devient +`/dev/ttyACM0`. Les cartes équipées d'une puce de conversion USB-série +(CH340, CP2102, FT232), fréquentes sur les clones et les anciennes +cartes ESP32, deviennent `/dev/ttyUSB0`. Pour accéder à ces fichiers sans +`sudo`, l'utilisateur doit appartenir au groupe `dialout` (voir +chapitre Arduino 2). + +**Le mode parasite.** La DS18B20 peut aussi fonctionner avec deux fils +seulement (`DATA` et la masse) : elle se nourrit alors sur la ligne de +données. Ce « mode parasite » complique le montage et la lecture ; le +projet utilise l'alimentation normale, à trois fils. + +**Les fiches techniques.** Les caractéristiques citées dans ce chapitre +viennent des documents des fabricants : la fiche *ESP32-S3 Series +Datasheet* d'Espressif, la page *Getting Started with Seeed Studio XIAO +ESP32S3* du wiki de Seeed Studio, la fiche de l'Arduino Uno R3 et la +fiche technique de la DS18B20. Prendre l'habitude de les consulter évite +bien des suppositions. + +## Exercices + +**Exercice 1** (avec la carte). Branchez la XIAO ESP32-S3 sur votre PC +Linux. Quelles commandes permettent de trouver le nom du port série et +de vérifier que le PC voit une puce ESP32-S3 ? Comment distinguer la +XIAO d'une autre carte ESP32-S3 ? + +**Exercice 2** (sans carte). Un camarade a câblé une sonde DS18B20 sur +une XIAO comme dans le tableau de câblage, mais il a supprimé la ligne +`pinMode(ONE_WIRE_PIN, INPUT_PULLUP);` de `setup()`. Il n'a pas soudé de +résistance externe. Que va-t-il observer, et pourquoi ? Proposez deux +corrections. + +**Exercice 3** (sans carte). Votre XIAO affiche en boucle +`WiFi toujours absent (statut 1)`, alors que le nom du réseau est +correct. Quelle est la cause la plus probable ? + +**Exercice 4** (sans carte). Pour chaque besoin, indiquez le fichier ou +le dossier du projet concerné : + +1. changer la broche à laquelle la sonde est reliée ; +2. ajouter une bibliothèque au projet ; +3. vérifier que les lectures à -127 sont bien rejetées ; +4. imprimer un boîtier ; +5. renseigner le mot de passe du WiFi. + +**Exercice 5** (sans carte). Clonez le dépôt du projet, placez-vous au +commit `1af068f` utilisé dans tout le module, et vérifiez que le dossier +du projet contient bien les fichiers listés dans ce chapitre. + +### Solutions + +**Solution 1.** + +```bash +ls /dev/ttyACM* +lsusb +``` + +`ls /dev/ttyACM*` affiche le port créé par la carte, en général +`/dev/ttyACM0`. Une façon fiable de le confirmer : lancer la commande +avant et après avoir branché la carte, et comparer. Dans la sortie de +`lsusb`, la carte se reconnaît à l'identifiant `303a:1001`. Cet +identifiant est celui de toutes les puces ESP32-S3 : il ne permet pas de +distinguer la XIAO d'une autre carte. Le README du projet lit pour cela +la taille de la mémoire flash avec `esptool` (`flash_id`) : 8 Mo pour la +XIAO ESP32-S3. + +**Solution 2.** Sans résistance de tirage, la ligne `DATA` n'est +ramenée à 3,3 V par rien : elle flotte, et les bits envoyés par la +sonde sont illisibles. La sonde peut ne pas être détectée +(`Sondes detectees : 0`), ou les lectures valent -127 et sont rejetées. +Corrections : remettre la ligne `pinMode(ONE_WIRE_PIN, INPUT_PULLUP);` +avant `sensors.begin()`, ou souder une résistance de 4,7 kilohms entre +`DATA` et `3V3`. La seconde solution est préférable avec un câble de +plusieurs mètres. + +**Solution 3.** L'antenne n'est pas branchée sur le connecteur U.FL. La +XIAO ESP32-S3 n'a pas d'antenne intégrée : sans elle, aucun réseau n'est +visible, d'où le statut 1 (`WL_NO_SSID_AVAIL`). Autre cause possible : +un réseau hors de portée. + +**Solution 4.** + +1. `src/main.cpp` (la constante `ONE_WIRE_PIN`). +2. `platformio.ini` (voir chapitre Arduino 2). +3. `lib/temp_logic/temp_logic.cpp` pour le code, `test/test_reading/` + pour les tests. +4. `case/` (fichiers STL). +5. `src/credentials.h`, absent du dépôt, à créer à partir du modèle + (voir chapitre Arduino 4). + +**Solution 5.** + +```bash +git clone https://github.com/pguinet/arduino.git +cd arduino +git switch --detach 1af068f +``` + +La dernière commande répond : + +``` +HEAD is now at 1af068f CI: move actions to their Node 24 majors +``` + +`--detach` est nécessaire parce que `1af068f` est un commit, pas une +branche : Git se place dessus sans branche courante (« HEAD détachée »). +On vérifie ensuite le contenu : + +```bash +cd sketches/XIAO-ESP32-S3/DS18B20_MQTT +find . -type f | sort +``` + +La liste est celle de la section « Le projet DS18B20_MQTT ». Pour revenir à la +version la plus récente : `git switch main`. From 964b2628890032d28907fcbd8f98d87f7634bee4 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:13:39 +0200 Subject: [PATCH 05/12] =?UTF-8?q?docs:=20chapitre=20Arduino=202=20(de=20l'?= =?UTF-8?q?IDE=20=C3=A0=20PlatformIO)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../module_arduino/02_ide_vers_platformio.md | 539 +++++++++++++++++- 1 file changed, 537 insertions(+), 2 deletions(-) diff --git a/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md index a4a9836..ff07be2 100644 --- a/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md +++ b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md @@ -1,5 +1,540 @@ # Arduino 2 — De l'IDE Arduino à PlatformIO -> **Objectifs** : à rédiger. +> **Objectifs** : à la fin de ce chapitre, vous saurez : +> - expliquer ce que l'IDE Arduino fait à votre place, et pourquoi un projet `.ino` est difficile à reproduire ; +> - installer PlatformIO, avec VS Code ou en ligne de commande ; +> - lire un fichier `platformio.ini` : environnements, plateforme, carte, bibliothèques versionnées ; +> - compiler, téléverser et ouvrir le moniteur série avec `pio run` et `pio device monitor` ; +> - distinguer les bibliothèques du registre (`lib_deps`) et les bibliothèques locales (`lib/`). > -> **Durée de lecture** : à préciser. +> **Durée de lecture** : environ 1 h, exercices compris. Seuls le téléversement et le moniteur série demandent la carte. + +--- + +Avec l'IDE Arduino, un projet tient dans un fichier `.ino`. On choisit +la carte dans un menu, on installe les bibliothèques depuis le +gestionnaire, puis on clique sur « Téléverser ». C'est simple, mais une +bonne partie du projet n'est écrite nulle part. + +Ce chapitre montre ce que l'IDE cache, puis présente **PlatformIO**, +l'outil du projet `DS18B20_MQTT`, où un fichier décrit tout ce qu'il +faut pour compiler, sur n'importe quelle machine. + +## Ce que l'IDE fait sans le dire + +Prenons un « Blink » ordinaire, qui range le clignotement dans une +fonction placée après `loop()` : + +```cpp +void setup() { + pinMode(LED_BUILTIN, OUTPUT); +} + +void loop() { + clignoter(500); +} + +void clignoter(unsigned long duree) { + digitalWrite(LED_BUILTIN, HIGH); + delay(duree); + digitalWrite(LED_BUILTIN, LOW); + delay(duree); +} +``` + +Sous le nom `main.ino`, ce fichier compile. Renommé en `main.cpp`, le +même texte échoue (sortie abrégée) : + +``` +src/main.cpp:2:11: error: 'LED_BUILTIN' was not declared in this scope +... +src/main.cpp:2:3: error: 'pinMode' was not declared in this scope +src/main.cpp:6:3: error: 'clignoter' was not declared in this scope +... +*** [.pio/build/seeed_xiao_esp32s3/src/main.cpp.o] Error 1 +``` + +Un `.ino` n'est pas du C++ : avant de compiler, l'IDE le transforme +en `.cpp`, en silence : + +1. **Il ajoute `#include `**, l'en-tête qui déclare + `pinMode`, `digitalWrite`, `delay`, `HIGH`... +2. **Il génère les prototypes.** En C++, une fonction doit être + déclarée avant d'être appelée ; l'IDE insère donc + `void clignoter(unsigned long duree);` en haut du fichier. +3. **Il concatène les onglets** : plusieurs `.ino` deviennent un seul + `.cpp`. + +En C++ ordinaire, ces deux lignes sont à écrire vous-même : + +```cpp +#include + +void clignoter(unsigned long duree); +``` + +Avec elles, `main.cpp` compile. Un fichier qui déclare ce qu'il +utilise se découpe et se teste mieux (voir chapitre Arduino 4). + +L'IDE cache aussi deux informations qui ne sont pas dans le sketch : + +- **La carte.** Elle est choisie dans le menu « Outils », avec ses + options. Le sketch ne dit pas pour quelle carte il a été écrit. +- **Les bibliothèques.** Le gestionnaire les installe dans un dossier + global (`~/Arduino/libraries` sous Linux), partagé par tous vos + sketches, dans la version du jour. Le sketch dit `#include + `, mais pas quelle version il attend. + +Conséquence : copié sur un autre PC, le sketch peut ne pas compiler, +ou compiler avec d'autres versions et se comporter autrement. Un +serveur d'intégration continue, parti d'une machine vierge, ne sait +pas quoi installer. + +## PlatformIO + +**PlatformIO** est un outil de développement pour l'embarqué, en deux +parties : **PlatformIO Core**, programme en ligne de commande nommé +`pio`, et **PlatformIO IDE**, extension de l'éditeur VS Code qui ajoute +des boutons au-dessus de `pio`. À partir d'un fichier de configuration, +`pio` télécharge lui-même la **plateforme** (support d'une famille de +puces, ici l'ESP32), la **chaîne de compilation**, le **framework** (le +core Arduino) et les **bibliothèques**. Rien ne s'installe à la main. + +### Installation + +PlatformIO tourne sous Linux, Windows et macOS. Au choix : + +- **Avec VS Code (recommandé pour débuter).** Installez VS Code, puis, + dans l'onglet « Extensions », l'extension **PlatformIO IDE**. Au + premier lancement, elle installe PlatformIO Core dans + `~/.platformio`. Sous Linux, elle demande Python 3 (paquet + `python3-venv` sous Debian et Ubuntu). +- **En ligne de commande seule.** La documentation officielle + recommande son script d'installation : + + ```bash + curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py + python3 get-platformio.py + ``` + +Dans les deux cas, `pio` est installé dans `~/.platformio/penv/bin/` +(`%USERPROFILE%\.platformio\penv\Scripts\` sous Windows, dossier à +ajouter à la variable `Path`). Sous Linux et macOS, un lien le rend +accessible depuis tout terminal ; VS Code propose aussi son propre +terminal PlatformIO : + +```bash +mkdir -p ~/.local/bin && ln -s ~/.platformio/penv/bin/pio ~/.local/bin/pio +``` + +Sous Linux, l'accès au port série demande en plus un réglage (règles +udev ou groupe `dialout`), décrit dans « Pour aller plus loin ». + +## L'arborescence d'un projet + +Un projet PlatformIO est un dossier qui contient un `platformio.ini` : + +``` +mon_projet/ ++-- platformio.ini <- la configuration (carte, bibliothèques...) ++-- src/ <- le programme : main.cpp ++-- include/ <- en-têtes partagés par les fichiers de src/ ++-- lib/ <- bibliothèques propres au projet ++-- test/ <- tests unitaires (voir chapitre Arduino 5) ++-- .pio/ <- fabriqué par pio : compilations, bibliothèques +``` + +`.pio/` se reconstruit à chaque compilation : il ne se versionne pas, +et le `.gitignore` du dépôt `arduino` contient la ligne `.pio/` (voir +module additionnel Git). `DS18B20_MQTT` n'a pas de dossier `include/`. + +## Le fichier `platformio.ini` + +Voici celui du projet, en entier +([source](https://github.com/pguinet/arduino/blob/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT/platformio.ini)) : + +```ini +; DS18B20_MQTT - XIAO ESP32-S3 +; Sonde DS18B20 publiee dans Home Assistant (MQTT Discovery) + +[platformio] +default_envs = xiao + +[env:xiao] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip +board = seeed_xiao_esp32s3 +framework = arduino +upload_port = /dev/ttyACM0 +monitor_port = /dev/ttyACM0 +monitor_speed = 115200 + +lib_deps = + paulstoffregen/OneWire@^2.3.8 + milesburton/DallasTemperature@^4.0.6 + knolleary/PubSubClient@^2.8 + bblanchon/ArduinoJson@^7.0.0 + +check_tool = cppcheck +check_flags = cppcheck: --enable=warning,style,performance --inline-suppr --suppress=*:*/.pio/* +check_src_filters = + + + +[env:native] +platform = native +build_flags = -std=c++17 -Wall -Wextra +lib_deps = + bblanchon/ArduinoJson@^7.0.0 +``` + +C'est un fichier INI : sections entre crochets, lignes `clé = valeur`, +commentaires après `;`, valeurs multiples sur des lignes indentées. + +**Les environnements.** Chaque section `[env:NOM]` décrit une façon de +compiler : `xiao` pour la carte, `native` pour les tests sur le PC +(voir chapitre Arduino 5). Dans `[platformio]`, `default_envs = xiao` +désigne l'environnement utilisé quand on n'en précise pas. + +**La plateforme.** Plutôt que `platform = espressif32` (la dernière +version publiée), le projet désigne par URL une version précise +(51.03.07) de **pioarduino**, plateforme communautaire qui fournit la +version 3 du core Arduino pour ESP32. La compilation de demain sera +identique à celle d'aujourd'hui. + +**La carte et le framework.** `board` remplace le menu « Outils > Type +de carte » de l'IDE (processeur, flash, brochage `D0`, `LED_BUILTIN`...). +`framework = arduino` choisit le framework Arduino plutôt qu'ESP-IDF, +le framework natif d'Espressif. + +**Le port et le moniteur.** `upload_port` et `monitor_port` fixent le +port (sans eux, PlatformIO le cherche seul) : on ne flashe pas la +mauvaise carte. Sous Windows, ce serait `COM3` par exemple ; sous macOS, +`/dev/cu.usbmodem...`. `monitor_speed` doit égaler le +`Serial.begin(115200)` du programme : sinon, le moniteur écoute à +9600 bauds et n'affiche que des caractères illisibles. + +**La suite.** `lib_deps` liste les bibliothèques (voir plus bas). Les +lignes `check_*` configurent cppcheck, lancé par `pio check` (voir +chapitre Arduino 6). `platform = native` compile pour le PC lui-même, +sans framework Arduino (voir chapitre Arduino 5). + +## Compiler, téléverser, observer + +Les commandes se lancent dans le dossier qui contient `platformio.ini`. + +| Commande | Rôle | Bouton VS Code | +|----------|------|----------------| +| `pio run -e xiao` | Compiler | Build (coche) | +| `pio run -e xiao -t upload` | Compiler et téléverser | Upload (flèche) | +| `pio device monitor` | Moniteur série | Serial Monitor (prise) | +| `pio run -t clean` | Effacer les fichiers compilés | Clean (corbeille) | +| `pio pkg list` | Lister plateforme, outils et bibliothèques | - | + +`-e` choisit l'environnement, `-t` (*target*) l'action après la +compilation. Sans `-e`, `pio run` traite `default_envs`, ici `xiao`. + +La première compilation télécharge tout et compile le core Arduino : +comptez une à plusieurs minutes. Les suivantes ne recompilent que les +fichiers modifiés. Sortie abrégée de `pio run -e xiao` dans le projet : + +``` +Processing xiao (platform: https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip; board: seeed_xiao_esp32s3; framework: arduino) +-------------------------------------------------------------------------------- +Library Manager: ArduinoJson@7.4.3 has been installed! +... +PLATFORM: Espressif 32 (51.3.6) > Seeed Studio XIAO ESP32S3 +HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash +... +Dependency Graph +|-- OneWire @ 2.3.8 +... +|-- temp_logic +|-- WiFi @ 3.0.7 +... +RAM: [= ] 12.7% (used 41728 bytes from 327680 bytes) +Flash: [=== ] 26.7% (used 893329 bytes from 3342336 bytes) +========================= [SUCCESS] Took 35.94 seconds ========================= +``` + +Les bibliothèques sont téléchargées dans `.pio/libdeps/xiao/`, dans le +projet (`^7.0.0` résolu en `7.4.3`). Suivent la plateforme et la carte, +les bibliothèques réellement utilisées, dont `temp_logic` (locale) et +`WiFi` (fournie par le core), puis l'occupation mémoire, comme en bas +de l'IDE. La compilation demande aussi `src/credentials.h`, absent du +dépôt (voir chapitre Arduino 4). + +Carte branchée, `-t upload` téléverse par `upload_port`, et +`pio device monitor` affiche ce que la carte écrit avec `Serial.print` +(`Ctrl+C` pour quitter). Les deux s'enchaînent : + +```bash +pio run -e xiao -t upload && pio device monitor +``` + +## Les bibliothèques + +**Les bibliothèques du registre, dans `lib_deps`.** Le registre +PlatformIO (https://registry.platformio.org) recense les bibliothèques +publiques, désignées par `auteur/nom@version`. Dans +`paulstoffregen/OneWire@^2.3.8` : + +`paulstoffregen` est le propriétaire (deux bibliothèques peuvent +porter le même nom), `OneWire` le nom, et `^2.3.8` la contrainte de +version : le caret accepte 2.3.8 ou plus, mais strictement moins de +3.0.0. Une version majeure peut casser l'interface : on l'accepte +explicitement, en modifiant `platformio.ini`. Les dépendances des +dépendances s'installent aussi (voir « Pour aller plus loin »). + +**Les bibliothèques locales, dans `lib/`.** Chaque sous-dossier de +`lib/` est une bibliothèque du projet, ici `lib/temp_logic/`. Rien à +déclarer : quand `src/main.cpp` écrit `#include "temp_logic.h"`, +PlatformIO trouve l'en-tête, compile la bibliothèque et la lie au +programme (ligne `|-- temp_logic` du graphe). Pourquoi ce découpage : +voir chapitre Arduino 4. + +## L'essentiel + +| Commande / Notion | Usage | +|-------------------|-------| +| `.ino` | Fichier de l'IDE, transformé en `.cpp` avant compilation | +| `#include ` | Déclare l'API Arduino ; obligatoire dans un `.cpp` | +| Prototype | Déclaration d'une fonction avant son appel (`void f(int);`) | +| PlatformIO Core (`pio`) | Outil en ligne de commande qui compile, téléverse, teste | +| PlatformIO IDE | Extension VS Code au-dessus de `pio` | +| `99-platformio-udev.rules`, `dialout` | Accès au port série sous Linux | +| `platformio.ini` | Configuration du projet : environnements, carte, bibliothèques | +| `[env:NOM]` | Environnement : une façon de compiler (`xiao`, `native`) | +| `default_envs` | Environnement(s) utilisé(s) sans `-e` | +| `platform`, `board`, `framework` | Famille de puces, carte, framework | +| `monitor_speed` | Vitesse du moniteur, égale à celle de `Serial.begin` | +| `lib_deps` | Bibliothèques du registre, `auteur/nom@^x.y.z` | +| `^2.3.8` | Version 2.3.8 ou plus, inférieure à 3.0.0 | +| `lib/` | Bibliothèques locales du projet | +| `.pio/` | Dossier de travail de `pio`, à ne pas versionner | +| `pio run -e xiao` | Compiler l'environnement `xiao` | +| `pio run -e xiao -t upload` | Compiler et téléverser | +| `pio device monitor` | Moniteur série (`Ctrl+C` pour quitter) | +| `pio run -t clean` | Effacer les fichiers compilés | +| `pio pkg list` | Lister les paquets et bibliothèques installés | + +## Pour aller plus loin + +**Accès au port série sous Linux.** Un utilisateur ordinaire ne peut +pas écrire sur `/dev/ttyACM0` (voir chapitre Arduino 1). PlatformIO +fournit des **règles udev** qui ouvrent l'accès aux cartes connues, +dont `303a:1001` : + +```bash +curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +Autre solution : ajouter votre compte au groupe `dialout`, propriétaire +du port sous Debian et Ubuntu (voir chapitre 5.1 du cours de base), +avec `sudo usermod -a -G dialout $USER`, puis rouvrir la session. Sous +Windows et macOS, il n'y a rien à faire. + +**Les dépendances indirectes.** `pio pkg list -e xiao` montre que +`DallasTemperature` a besoin de `OneWire`, installée même si elle +n'était pas dans `lib_deps` (arborescence redessinée en ASCII) : + +``` +Libraries ++-- ArduinoJson @ 7.4.3 (required: bblanchon/ArduinoJson @ ^7.0.0) ++-- DallasTemperature @ 4.0.6 (required: milesburton/DallasTemperature @ ^4.0.6) +| +-- OneWire @ 2.3.8 (required: paulstoffregen/OneWire @ ^2.3.5) ++-- PubSubClient @ 2.8.0 (required: knolleary/PubSubClient @ ^2.8) +``` + +**Une URL `stable` n'est pas une version figée.** Selon votre +installation, `pio project init` peut écrire dans le `platformio.ini` +une URL de pioarduino qui se termine par `/stable/`, c'est-à-dire « la +dernière version stable ». Le contenu de cette URL change sans que le +fichier change. Lors de la rédaction de ce chapitre, avec PlatformIO +Core 6.1.19, elle désignait une version qui exigeait un Core plus +récent : + +``` +IncompatiblePlatform: Development platform 'espressif32' is not compatible with PlatformIO Core v6.1.19 and depends on PlatformIO Core >=6.2.0. +``` + +Un projet qui doit compiler dans un an, ou en CI, fige la version, +comme `DS18B20_MQTT` le fait avec `51.03.07`. + +**D'autres contraintes de version.** `@2.3.8` exige exactement cette +version ; `@~2.3.8` accepte 2.3.x, à partir de 2.3.8 (correctifs +seulement) ; `@^2.3.8` accepte 2.x.y. Pour une version `0.x`, le caret +est plus strict : `^0.5.1` n'accepte que des 0.5.x. On peut aussi +désigner un dépôt Git (`https://github.com/auteur/projet.git#v1.2.0`). + +**Partager la configuration entre environnements.** Une section `[env]` +sans nom est héritée par tous les `[env:...]`. Une option peut aussi +reprendre celle d'une autre section : `lib_deps = ${env:xiao.lib_deps}`. + +**Voir la configuration calculée.** `pio project config` affiche la +configuration après héritage et substitution, environnement par +environnement. `pio run -e xiao --list-targets` liste les actions +possibles pour `-t` (`upload`, `erase`, `size`, `uploadfs`...). + +**Les ports.** `pio device list` liste les ports série et leur +identifiant matériel. Les deux options `--upload-port` de `pio run` et +`-p` de `pio device monitor` remplacent ponctuellement le port du +fichier. + +**Le téléversement de la XIAO ESP32-S3.** Si le programme en place +plante au démarrage, le port USB natif peut disparaître avant le +téléversement. On force alors le mode de téléversement : maintenir le +bouton `B` (BOOT), brancher la carte, relâcher, puis relancer +`pio run -e xiao -t upload`. + +**Garder des `.ino` ?** PlatformIO sait compiler un `.ino` placé dans +`src/` : il fait la même conversion que l'IDE (« Converting main.ino » +dans la sortie). C'est pratique pour porter un vieux sketch, mais un +projet qui grandit gagne à passer en `.cpp`. + +**La CLI de l'IDE.** `arduino-cli` est la version en ligne de commande +de l'IDE Arduino. Elle compile et téléverse sans fenêtre, mais garde le +modèle de l'IDE : bibliothèques globales, carte passée en option à +chaque commande. + +## Exercices + +**Exercice 1 -- Porter un Blink (sans carte).** Créez un projet vide +dans un nouveau dossier : + +```bash +mkdir -p ~/essais/blink && cd ~/essais/blink +pio project init --board seeed_xiao_esp32s3 +``` + +Remplacez la ligne `platform` du `platformio.ini` créé par celle du +projet `DS18B20_MQTT`. Copiez le Blink du début du chapitre dans +`src/main.cpp`, compilez avec `pio run`, puis corrigez le fichier +jusqu'à ce que la compilation réussisse. + +**Exercice 2 -- Ajouter une bibliothèque (sans carte).** Dans le projet +de l'exercice 1, déclarez `bblanchon/ArduinoJson@^7.0.0` dans +`lib_deps`. Écrivez un `main.cpp` qui construit un `JsonDocument` +contenant une température et l'écrit sur `Serial` toutes les 2 +secondes, puis compilez. Où la bibliothèque a-t-elle été installée ? +Quelle ligne faut-il aussi ajouter pour lire ce message avec +`pio device monitor` ? + +**Exercice 3 -- Contraintes de version.** Pour chaque ligne de +`lib_deps` du projet, donnez la plus petite et la plus grande version +acceptées. La version 3.0.0 de `OneWire` serait-elle installée ? Et +`PubSubClient` 2.8.1 ? + +**Exercice 4 -- Les environnements (sans carte).** Dans le projet +`DS18B20_MQTT`, quels sont les environnements ? Que compile `pio run` +sans option ? Que se passe-t-il avec `pio run -e native`, et pourquoi +cet environnement sert-il malgré tout ? + +**Exercice 5 -- Reproductibilité.** Un collègue vous envoie un sketch +`.ino` qui « marche chez lui ». Listez les informations qui vous +manquent pour le compiler à l'identique, et indiquez où chacune se +trouverait dans un projet PlatformIO. + +### Solutions + +**Solution 1.** `pio project init` crée `platformio.ini`, `src/`, +`lib/`, `include/` et `test/`, avec un environnement +`[env:seeed_xiao_esp32s3]`. La ligne `platform` générée dépend de votre +installation. Si la commande se termine par `IncompatiblePlatform` (voir +« Pour aller plus loin »), les fichiers sont tout de même créés. Dans +tous les cas, on remplace cette ligne par : + +```ini +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip +``` + +La première compilation échoue sur `pinMode`, `LED_BUILTIN`, +`clignoter`... Il faut ajouter l'include et le prototype : + +```cpp +#include + +void clignoter(unsigned long duree); + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); +} +... +``` + +Avec `#include ` seul, il reste une erreur : +`'clignoter' was not declared in this scope`. Avec les deux lignes, la +compilation se termine par `[SUCCESS]`. Autre solution : déplacer la +fonction `clignoter()` avant `loop()`. + +**Solution 2.** Le `platformio.ini` se termine par : + +```ini +lib_deps = + bblanchon/ArduinoJson@^7.0.0 +monitor_speed = 115200 +``` + +Un `main.cpp` possible : + +```cpp +#include +#include + +void setup() { + Serial.begin(115200); +} + +void loop() { + JsonDocument doc; + doc["temperature"] = 21.5; + doc["unite"] = "C"; + serializeJson(doc, Serial); + Serial.println(); + delay(2000); +} +``` + +`pio run` affiche `Library Manager: ArduinoJson@7.4.3 has been +installed!` (ou une version 7.x plus récente) et la bibliothèque est +rangée dans `.pio/libdeps/seeed_xiao_esp32s3/`, à l'intérieur du +projet. `monitor_speed = 115200` est nécessaire, car le moniteur écoute +à 9600 bauds par défaut ; la lecture elle-même demande la carte. + +**Solution 3.** + +| Bibliothèque | Minimum | Maximum (exclu) | +|--------------|---------|-----------------| +| `OneWire@^2.3.8` | 2.3.8 | 3.0.0 | +| `DallasTemperature@^4.0.6` | 4.0.6 | 5.0.0 | +| `PubSubClient@^2.8` | 2.8.0 | 3.0.0 | +| `ArduinoJson@^7.0.0` | 7.0.0 | 8.0.0 | + +`OneWire` 3.0.0 est refusée : c'est une nouvelle version majeure. +`PubSubClient` 2.8.1 est acceptée. + +**Solution 4.** Deux environnements : `xiao` et `native`. `pio run` sans +`-e` compile `default_envs`, donc `xiao` seulement. `pio run -e native` +essaie de compiler `src/main.cpp` pour le PC et échoue : + +``` +src/main.cpp:12:10: fatal error: Arduino.h: No such file or directory +``` + +Le PC n'a pas de framework Arduino. L'environnement `native` ne sert +pas à compiler le programme, mais à exécuter les tests de `test/` avec +`pio test -e native` : ils n'utilisent que `lib/temp_logic`, qui ne +dépend pas d'Arduino (voir chapitre Arduino 5). + +**Solution 5.** Il manque : + +- la carte et ses options (`board` dans `[env:...]`) ; +- la version du core, c'est-à-dire de la plateforme (`platform`, figée + par une version ou une URL) ; +- la liste des bibliothèques et leurs versions (`lib_deps`) ; +- la vitesse du port série (`monitor_speed`) ; +- les éventuels autres fichiers `.ino` du sketch, que l'IDE concatène + (dans un projet PlatformIO, ce sont des fichiers de `src/` ou des + bibliothèques de `lib/`). + +Tout cela tient dans `platformio.ini` et dans le dossier du projet, +versionnés ensemble avec Git. From 6a61b8beca9ed33c3484540d9cd1d620aaf1aa88 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:13:39 +0200 Subject: [PATCH 06/12] docs: chapitre Arduino 3 (MQTT et Home Assistant) Co-Authored-By: Claude Opus 5.5 --- .../module_arduino/03_mqtt_home_assistant.md | 469 +++++++++++++++++- 1 file changed, 467 insertions(+), 2 deletions(-) diff --git a/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md index dd19d9f..7cc8757 100644 --- a/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md +++ b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md @@ -1,5 +1,470 @@ # Arduino 3 — MQTT et Home Assistant -> **Objectifs** : à rédiger. +> **Objectifs** : à la fin de ce chapitre, vous saurez : +> - expliquer le modèle publication/abonnement et le rôle d'un broker MQTT ; +> - écrire un sujet et un filtre d'abonnement avec les jokers `+` et `#` ; +> - dire ce que changent la QoS, un message retenu et un testament ; +> - lire le message de découverte qui crée le capteur dans Home Assistant ; +> - associer chaque sujet du projet à son contenu. > -> **Durée de lecture** : à préciser. +> **Durée de lecture** : environ 1 h. + +--- + +La carte mesure une température ; Home Assistant l'affiche. Entre les +deux, pas d'appel direct : la carte envoie ses mesures à un serveur +intermédiaire, où Home Assistant les lit. Ce serveur parle **MQTT**, un +protocole léger, très répandu dans l'Internet des objets. + +Ce chapitre présente MQTT juste assez pour comprendre le code, puis la +**découverte** (*MQTT Discovery*), qui fait apparaître le capteur dans +Home Assistant sans aucune configuration de ce côté. Les exercices se +font sans carte ; le dernier demande Docker. + +## Le besoin : publier sans connaître ses lecteurs + +Une sonde peut intéresser plusieurs programmes : Home Assistant, un +script d'archivage, une alerte. Si la carte devait les connaître tous, +il faudrait la reprogrammer à chaque nouveau lecteur. + +MQTT suit le modèle **publication/abonnement** (*publish/subscribe*) : +la carte **publie** ses mesures sous un nom, sans savoir qui les lira ; +chaque programme intéressé **s'abonne** à ce nom. Au centre, un serveur +reçoit les publications et les distribue aux abonnés : + +``` + abonnés + +-----------+ +-> Home Assistant + | XIAO | +--------+ | + | ESP32-S3 |->| broker |-+-> script d'archive + | + DS18B20 | | MQTT | | + +-----------+ +--------+ +-> mosquitto_sub + publie +``` + +La carte et les lecteurs ne connaissent que l'adresse du broker : on +ajoute ou retire un lecteur sans toucher à la carte. + +## Le vocabulaire de MQTT + +**Broker.** Le serveur central, qui écoute par défaut sur le port TCP +1883. Le plus courant est **Mosquitto**, logiciel libre de la fondation +Eclipse. Dans le projet, son adresse vient de `MQTT_SERVER` et +`MQTT_PORT` (voir chapitre Arduino 4 pour les secrets). + +**Client.** Tout programme connecté au broker, pour publier, s'abonner, +ou les deux : la carte (bibliothèque `PubSubClient`) comme Home +Assistant. Chaque client a un **identifiant** unique ; celui de la carte +est `xiao_ds18b20_` suivi de trois octets de sa MAC, par exemple +`xiao_ds18b20_3bb004`. + +**Sujet** (*topic*). Le nom sous lequel un message est publié, découpé +en niveaux par des `/` comme un chemin de fichier. Il n'a pas besoin +d'être déclaré, et l'organisation des niveaux est une convention du +développeur ; ici, le type d'appareil, l'appareil, puis la donnée : + +``` +xiao_ds18b20/xiao_ds18b20_3bb004/temperature +``` + +**Filtre et jokers.** Un abonné donne un **filtre**, qui peut contenir +deux jokers : + +| Joker | Remplace | Exemple | Correspond à | +|-------|----------|---------|--------------| +| `+` | exactement un niveau | `xiao_ds18b20/+/status` | le `status` de chaque carte | +| `#` | tous les niveaux restants (en dernier) | `xiao_ds18b20/#` | tout ce que publient les cartes | + +`+` peut apparaître plusieurs fois ; `#` seulement à la fin. S'abonner à +`#` seul reçoit tout le trafic du broker : pratique pour observer, à +éviter en production. + +**Message.** Un sujet et un contenu (*payload*), simple suite d'octets +dont MQTT n'impose pas le format. Le projet publie du texte (`21.4`, +`online`) et du JSON pour la découverte. + +**QoS** (*Quality of Service*). Le niveau de garantie de livraison, +choisi à chaque publication : + +| QoS | Garantie | Coût | +|-----|----------|------| +| 0 | au plus une fois : envoyé sans accusé de réception | le plus léger | +| 1 | au moins une fois : renvoyé jusqu'à l'accusé, doublons possibles | un aller-retour | +| 2 | exactement une fois | deux allers-retours | + +Le projet publie tout en QoS 0, la seule que `PubSubClient` sait +publier : perdre une mesure envoyée toutes les 10 secondes est sans +importance. + +**Session.** Ce que le broker garde pour un client : ses abonnements et, +en QoS 1 ou 2, les messages en attente. La carte demande une session +« propre » (*clean session*), oubliée à la déconnexion : elle se +réabonne donc après chaque connexion (voir chapitre Arduino 4). + +## Les messages retenus + +Par défaut, un message n'est remis qu'aux clients abonnés **au moment** +de la publication. Un message publié avec le drapeau **retenu** +(*retained*) est en plus conservé par le broker, qui le remet aussitôt à +tout nouvel abonné. Le broker garde un seul message retenu par sujet, le +dernier. + +Dans `PubSubClient`, le drapeau est le dernier argument de `publish`. +Comparez ces deux lignes de `src/main.cpp` : + +```cpp +mqtt.publish(topics.status, "online", true); +``` + +```cpp +if (mqtt.publish(topics.state, value)) { +``` + +La première est retenue (`true`), la seconde non. La règle : + +- un **état**, vrai tant qu'il n'a pas changé (« la carte est en + ligne », « voici comment me décrire »), est retenu : un client qui + arrive doit le connaître tout de suite ; +- une **mesure** n'est pas retenue : retenir `21.4` le ferait passer pour + la température actuelle, même une semaine après la panne de la carte. + +Un message **vide** retenu efface le message retenu d'un sujet : c'est +le remède aux « appareils fantômes » du README, laissés par une carte +dont l'identifiant a changé. + +## Le testament + +Une carte qui perd son alimentation ne prévient personne : son sujet +`status`, retenu, resterait à `online`. MQTT prévoit le **testament** +(*Last Will and Testament*, LWT) : à la connexion, le client confie au +broker un message à publier **à sa place** s'il disparaît sans se +déconnecter proprement. Le broker s'en aperçoit quand la connexion TCP +tombe, ou après une fois et demie l'intervalle *keepalive* sans nouvelle +(15 secondes par défaut dans `PubSubClient`). + +Le testament se donne dans l'appel de connexion de `handleMqtt()`, dans +`src/main.cpp` : + +```cpp +const bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS, topics.status, 0, true, "offline"); +``` + +| Argument | Valeur | Rôle | +|----------|--------|------| +| 1 | `deviceId` | identifiant du client, unique sur le broker | +| 2 | `MQTT_USER` | nom d'utilisateur (secret, voir chapitre Arduino 4) | +| 3 | `MQTT_PASS` | mot de passe | +| 4 | `topics.status` | sujet du testament : `xiao_ds18b20//status` | +| 5 | `0` | QoS du testament | +| 6 | `true` | testament retenu | +| 7 | `"offline"` | contenu du testament | + +Une fois connectée, la carte publie elle-même `online`, retenu, sur ce +sujet. `status` vaut donc toujours l'état réel : `online` publié par la +carte, puis `offline` publié par le broker dès qu'elle disparaît. Le +testament doit être retenu, sinon un client arrivé après la panne ne +verrait jamais `offline`. + +## Home Assistant et MQTT Discovery + +**Home Assistant** est un logiciel libre de domotique ; son intégration +MQTT est un client comme un autre. Avec **MQTT Discovery**, c'est la +carte qui se décrit auprès de lui : Home Assistant +s'abonne à `homeassistant/#`, et un message publié sur un sujet de la +forme `homeassistant////config` crée l'entité +correspondante. La carte publie sa description, retenue, sur : + +``` +homeassistant/sensor/xiao_ds18b20_3bb004/temperature/config +``` + +Le contenu est un JSON construit par `buildDiscovery()`, dans +[`lib/temp_logic/ha_discovery.cpp`](https://github.com/pguinet/arduino/blob/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT/lib/temp_logic/ha_discovery.cpp). +Voici, indenté pour la lecture, celui qu'il produit pour +`xiao_ds18b20_3bb004` (la carte l'envoie sur une ligne de 533 octets) : + +```json +{ + "name": "Temperature", + "unique_id": "xiao_ds18b20_3bb004_temperature", + "state_topic": "xiao_ds18b20/xiao_ds18b20_3bb004/temperature", + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": "°C", + "suggested_display_precision": 1, + "availability_mode": "all", + "availability": [ + {"topic": "xiao_ds18b20/xiao_ds18b20_3bb004/status"}, + {"topic": "xiao_ds18b20/xiao_ds18b20_3bb004/probe"} + ], + "device": { + "identifiers": ["xiao_ds18b20_3bb004"], + "name": "xiao_ds18b20_3bb004", + "model": "XIAO ESP32S3 + DS18B20", + "manufacturer": "Seeed Studio" + } +} +``` + +Les champs à connaître : + +- `unique_id` : identifiant stable de l'entité ; Home Assistant + reconnaît ainsi le capteur d'une publication à l'autre ; +- `state_topic` : le sujet où lire la valeur ; +- `device_class` : la nature de la mesure (icône, regroupement) ; + `state_class: measurement` fait tenir des statistiques ; +- `unit_of_measurement`, `suggested_display_precision` : affichage en + °C avec un chiffre après la virgule ; +- `availability` : les sujets qui disent si le capteur est disponible, + avec les valeurs par défaut `online` et `offline` ; +- `availability_mode: all` : disponible seulement si **tous** ces sujets + valent `online`. La carte doit être connectée (`status`, tenu par le + testament) **et** la sonde doit répondre (`probe`, voir chapitre + Arduino 4). Sinon, Home Assistant affiche « indisponible » plutôt + qu'une ancienne valeur ; +- `device` : regroupe les entités sous un même appareil. + +Ce message dépasse les 256 octets acceptés par défaut par `PubSubClient`, +d'où le réglage de son tampon (voir chapitre Arduino 4). + +**Redémarrage de Home Assistant.** À sa connexion au broker, Home +Assistant publie `online` sur `homeassistant/status`. La carte, abonnée +à ce sujet, republie alors sa découverte et sa prochaine température +(fonction `onMqttMessage()`, voir chapitre Arduino 4). Les messages +étant retenus, c'est un filet de sécurité : il couvre par exemple un +broker redémarré sans conserver ses messages retenus. + +## Les sujets du projet + +Le tableau suivant, repris du README, résume ce que publie la carte ; +`` est son identifiant. Les noms sont construits par `makeTopics()`, +dans le même fichier que `buildDiscovery()`. + +| Sujet | Contenu | Retenu | +|---|---|---| +| `homeassistant/sensor//temperature/config` | découverte | oui | +| `xiao_ds18b20//temperature` | valeur, ex. `21.4` | non | +| `xiao_ds18b20//status` | `online` / `offline` (testament MQTT) | oui | +| `xiao_ds18b20//probe` | `online` / `offline` selon la sonde | oui | + +La carte s'abonne à un seul sujet, `homeassistant/status`. + +> **Et en vrai ?** Pour voir la carte publier, il faut un broker et un +> Home Assistant. Tous deux pourront tourner dans des conteneurs sur le +> futur homelab du club (voir module additionnel Docker). En attendant, +> n'importe quel broker convient ; cette commande affiche tout ce que +> publient les cartes, sujet compris (`-v`) : +> +> ``` +> mosquitto_sub -h -v -t 'xiao_ds18b20/#' +> ``` +> +> L'exercice 5 monte un broker local avec Docker, sans carte. + +## L'essentiel + +| Commande / Notion | Usage | +|-------------------|-------| +| Publication/abonnement | Les émetteurs publient, les lecteurs s'abonnent, sans se connaître | +| Broker (Mosquitto) | Serveur central qui reçoit et distribue les messages (port 1883) | +| Client | Programme connecté au broker, avec un identifiant unique | +| Sujet | Nom hiérarchique d'un message : `a/b/c` | +| `+` / `#` | Joker d'un niveau / de tous les niveaux restants | +| QoS 0, 1, 2 | Au plus une fois, au moins une fois, exactement une fois | +| Message retenu | Dernier message d'un sujet, remis à tout nouvel abonné | +| Message vide retenu | Efface le message retenu d'un sujet | +| Testament (LWT) | Message publié par le broker si le client disparaît | +| MQTT Discovery | Un JSON retenu sur `homeassistant/.../config` crée l'entité | +| `availability_mode: all` | Disponible seulement si tous les sujets d'`availability` valent `online` | +| `homeassistant/status` | `online` au démarrage de Home Assistant : la carte republie | +| `mosquitto_sub -v -t FILTRE` | Afficher les messages reçus, avec leur sujet | +| `mosquitto_pub -t SUJET -m TEXTE [-r]` | Publier un message (retenu avec `-r`) | + +## Pour aller plus loin + +**Sécuriser le broker.** Un broker ouvert à tous accepte n'importe quelle +publication, y compris une fausse découverte. En production, Mosquitto +exige un compte (`password_file`, fichier créé avec `mosquitto_passwd`) +et peut chiffrer les échanges en TLS, en général sur le port 8883. Le +projet transmet un nom d'utilisateur et un mot de passe, mais pas de +TLS : à réserver à un réseau local de confiance. + +**MQTT 3.1.1 et MQTT 5.** `PubSubClient` parle MQTT 3.1.1. La version 5 +ajoute notamment une durée de vie aux messages, des propriétés +utilisateur et des codes de retour plus précis. Les brokers récents +acceptent les deux versions en même temps. + +**QoS de bout en bout.** La QoS s'applique à chaque saut séparément : +de l'émetteur au broker, puis du broker à chaque abonné, avec la QoS +demandée par l'abonné. Un message publié en QoS 2 et lu par un abonné en +QoS 0 n'est livré qu'au plus une fois à cet abonné. + +**Persistance du broker.** Mosquitto garde ses messages retenus en +mémoire. Avec `persistence true` dans sa configuration, il les écrit +aussi sur disque et les retrouve après un redémarrage. C'est le cas que +couvre, sinon, la republication déclenchée par `homeassistant/status`. + +**Supprimer un appareil.** Publier un message vide retenu sur le sujet +`config` d'une entité la supprime de Home Assistant. Pour un appareil +fantôme du projet, on vide ainsi ses trois sujets retenus : + +``` +mosquitto_pub -h -t 'homeassistant/sensor//temperature/config' -r -n +mosquitto_pub -h -t 'xiao_ds18b20//status' -r -n +mosquitto_pub -h -t 'xiao_ds18b20//probe' -r -n +``` + +L'option `-n` publie un message vide. + +**Autres types d'entités.** Le niveau `` du sujet de découverte +peut valoir `binary_sensor` (ouvert/fermé), `switch` (un relais que Home +Assistant commande en publiant sur un `command_topic`), `light`, etc. La +documentation de l'intégration MQTT de Home Assistant décrit les champs +de chacun. + +## Exercices + +Tous les exercices se font sans carte. L'exercice 5 demande Docker. + +**Exercice 1.** La carte fonctionne depuis une heure. Un nouveau client +s'abonne à `xiao_ds18b20/#` et à `homeassistant/#`. Que reçoit-il +immédiatement, avant toute nouvelle publication ? Et si la carte a été +débranchée il y a dix minutes ? + +**Exercice 2.** Plusieurs cartes publient sur le même broker. Écrivez : + +1. le filtre qui reçoit la température de toutes les cartes, et rien + d'autre ; +2. le filtre qui reçoit tout ce que publie la seule carte + `xiao_ds18b20_3bb004` ; +3. le filtre qui reçoit les messages de découverte de tous les capteurs + annoncés à Home Assistant. + +**Exercice 3.** Pourquoi la température n'est-elle pas publiée en message +retenu, alors que `status` et `probe` le sont ? Décrivez ce que verrait +Home Assistant, au redémarrage, si elle l'était et que la carte était +hors service. + +**Exercice 4.** Dans chaque situation, dites ce que valent `status` et +`probe`, et ce qu'affiche Home Assistant : + +1. tout fonctionne ; +2. le fil DATA de la sonde est coupé, la carte reste connectée ; +3. la carte n'est plus alimentée. + +**Exercice 5 (Docker).** Simulez la carte avec un broker Mosquitto local +et les outils `mosquitto_pub` et `mosquitto_sub` fournis dans l'image : + +1. lancez le broker `eclipse-mosquitto:2` en autorisant les connexions + anonymes ; +2. publiez `status` et `probe` en messages retenus, puis une température + non retenue ; abonnez-vous ensuite à `xiao_ds18b20/#` et notez ce que + vous recevez ; +3. simulez la disparition brutale d'une carte qui a laissé un testament, + et vérifiez que `status` passe à `offline` ; +4. supprimez le broker. + +### Solutions + +**Solution 1.** Il reçoit immédiatement les messages retenus : le JSON +de découverte sur `homeassistant/sensor//temperature/config`, +`online` sur `status` et `online` sur `probe` (si la sonde répond). Il +ne reçoit pas la température, non retenue : il l'aura à la prochaine +publication. Si la carte est débranchée depuis dix minutes, le broker a +publié le testament : `status` vaut `offline`. La découverte et `probe` +(`online`, dernière valeur publiée) restent inchangés ; c'est +`availability_mode: all` qui rend malgré tout le capteur indisponible. + +**Solution 2.** + +1. `xiao_ds18b20/+/temperature` +2. `xiao_ds18b20/xiao_ds18b20_3bb004/#` +3. `homeassistant/+/+/+/config`, ou plus largement `homeassistant/#`, + qui reçoit aussi `homeassistant/status`. + +**Solution 3.** Une mesure n'a de sens qu'au moment où elle est prise. +Retenue, la dernière valeur serait remise à chaque nouvel abonné, même +longtemps après. Au redémarrage, Home Assistant lirait par exemple +`21.4` et le présenterait comme la valeur courante. Ici, le testament +fait passer `status` à `offline` et le capteur s'affiche +« indisponible », ce qui est exact. + +**Solution 4.** + +1. `status` = `online`, `probe` = `online` : la température s'affiche. +2. `status` = `online`, `probe` = `offline` après trois lectures + invalides (voir chapitre Arduino 4) : capteur indisponible. +3. `status` = `offline`, publié par le broker grâce au testament ; + `probe` garde sa dernière valeur, `online` : capteur indisponible, + puisque `availability_mode: all` exige que les deux sujets valent + `online`. + +**Solution 5.** + +1. Créez la configuration et lancez le broker : + + ```bash + mkdir mosquitto && cd mosquitto + printf 'listener 1883\nallow_anonymous true\n' > mosquitto.conf + docker run -d --name broker -p 1883:1883 \ + -v "$PWD/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" \ + eclipse-mosquitto:2 + docker logs broker + ``` + + Sans fichier de configuration, Mosquitto 2 n'accepte que les + connexions locales. Le journal se termine par : + + ``` + ...: Opening ipv4 listen socket on port 1883. + ...: Opening ipv6 listen socket on port 1883. + ...: mosquitto version 2.1.2 running + ``` + +2. Les outils sont lancés dans le conteneur avec `docker exec`. + L'option `-W 2` arrête l'abonné au bout de 2 secondes : + + ```bash + ID=xiao_ds18b20_3bb004 + docker exec broker mosquitto_pub -t xiao_ds18b20/$ID/status -m online -r + docker exec broker mosquitto_pub -t xiao_ds18b20/$ID/probe -m online -r + docker exec broker mosquitto_pub -t xiao_ds18b20/$ID/temperature -m 21.4 + docker exec broker mosquitto_sub -v -t 'xiao_ds18b20/#' -W 2 + ``` + + ``` + xiao_ds18b20/xiao_ds18b20_3bb004/status online + xiao_ds18b20/xiao_ds18b20_3bb004/probe online + Timed out + ``` + + Seuls les messages retenus arrivent : la température a été publiée + avant l'abonnement et n'a pas été conservée. + +3. Un client qui se termine normalement (Ctrl+C) se déconnecte + proprement, et le broker ne publie pas son testament. Il faut donc + le tuer brutalement (`kill -9`) pour simuler une coupure + d'alimentation : + + ```bash + docker exec broker sh -c "mosquitto_sub -i $ID -t rien \ + --will-topic xiao_ds18b20/$ID/status --will-payload offline \ + --will-retain & sleep 1; kill -9 \$!" + docker exec broker mosquitto_sub -v -t 'xiao_ds18b20/+/status' -W 1 + ``` + + ``` + xiao_ds18b20/xiao_ds18b20_3bb004/status offline + Timed out + ``` + + Pour suivre la scène en direct, lancez avant, dans un second + terminal, `docker exec broker mosquitto_sub -v -t 'xiao_ds18b20/#'` : + il affiche les deux messages retenus, puis `status offline` au + moment du `kill`. + +4. Supprimez le conteneur : + + ```bash + docker rm -f broker + ``` From a88063e9de4d576b4ca0ef2d12b3fc76f7437106 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:13:39 +0200 Subject: [PATCH 07/12] docs: chapitre Arduino 4 (lecture du code) Co-Authored-By: Claude Opus 5.5 --- .../module_arduino/04_lecture_code.md | 513 +++++++++++++++++- 1 file changed, 511 insertions(+), 2 deletions(-) diff --git a/supports/modules_additionnels/module_arduino/04_lecture_code.md b/supports/modules_additionnels/module_arduino/04_lecture_code.md index dcc5809..2e60cea 100644 --- a/supports/modules_additionnels/module_arduino/04_lecture_code.md +++ b/supports/modules_additionnels/module_arduino/04_lecture_code.md @@ -1,5 +1,514 @@ # Arduino 4 — Lecture du code -> **Objectifs** : à rédiger. +> **Objectifs** : à la fin de ce chapitre, vous saurez : +> - expliquer pourquoi le projet sépare le code matériel (`src/`) de la logique pure (`lib/`) ; +> - garder des secrets (WiFi, MQTT) hors d'un dépôt Git ; +> - écrire une boucle non bloquante avec `millis()` et justifier qu'elle survit au débordement du compteur ; +> - lire une machine à états simple (conversion asynchrone de la DS18B20) ; +> - expliquer la validation des lectures, `PublishPolicy` et `ProbeHealth` ; +> - suivre la reconnexion WiFi et MQTT sans bloquer le programme. > -> **Durée de lecture** : à préciser. +> **Durée de lecture** : ~1 h 30. + +--- + +Ce chapitre lit le programme lui-même : il en cite les passages qui +portent une décision, et explique **pourquoi** chaque choix a été fait. +Gardez ouverts `src/main.cpp` et `lib/temp_logic/temp_logic.cpp`, au +commit figé `1af068f` ; les numéros de ligne cités y renvoient : + +``` +https://github.com/pguinet/arduino/tree/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT +``` + +## Deux couches : le matériel et la logique + +Un sketch de l'IDE tient souvent dans un seul fichier, où lecture de la +sonde, calculs et envoi réseau se mélangent. Ce projet sépare le code +**matériel**, dans `src/main.cpp`, de la **logique pure**, dans +`lib/temp_logic/` : + +``` + src/main.cpp ------------------------> lib/temp_logic/ (temp::...) + | Arduino, WiFi, OneWire, | stdint.h, stddef.h, math.h, + | DallasTemperature, PubSubClient | stdio.h, stdlib.h, ArduinoJson + | (ESP32 uniquement) | (compilent aussi sur PC) +``` + +La logique n'appelle jamais le matériel ; même l'heure lui est **passée +en paramètre** (`nowMs`). Elle compile donc avec le `g++` de votre PC et +se teste sans carte, avec des valeurs choisies (voir chapitre Arduino +5). `main.cpp`, qui ne se teste qu'en flashant la carte, reste +**mince** : de la plomberie entre le matériel et la logique. + +L'espace de noms `temp` (`temp::isValidReading`...) signale ce qui vient +du code testé et évite les collisions avec une bibliothèque Arduino. + +## Les secrets hors du dépôt + +Le nom du WiFi, son mot de passe et les identifiants MQTT ne doivent pas +finir sur GitHub (`main.cpp`, lignes 19 à 28) : + +```cpp +#include "credentials.h" +... +#ifndef WIFI_SSID +#error "credentials.h : WIFI_SSID manquant (voir sketches/common/credentials.h.example)" +#endif +... +``` + +- **le fichier est ignoré** : le `.gitignore` du dépôt contient + `**/credentials.h` (voir module additionnel Git) ; +- **un modèle est versionné** : `sketches/common/credentials.h.example`, + avec des valeurs factices, à copier en `credentials.h` ; +- **le compilateur vérifie** : `#error` arrête la compilation avec un + message qui dit quoi faire. + +Voici `pio run -e xiao` quand `WIFI_SSID` manque : + +``` +src/main.cpp:24:2: error: #error "credentials.h : WIFI_SSID manquant (voir sketches/common/credentials.h.example)" +... +xiao FAILED 00:00:08.913 +``` + +Sans le fichier, l'erreur est +`fatal error: credentials.h: No such file or directory`. + +## setup() : préparer sans attendre + +Extraits de `setup()` (lignes 168 à 196) : + +```cpp + pinMode(ONE_WIRE_PIN, INPUT_PULLUP); + sensors.begin(); +... + sensors.setWaitForConversion(false); +... + esp_read_mac(mac, ESP_MAC_WIFI_STA); +... + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); // non bloquant +... + mqtt.setBufferSize(1024); // decouverte > 256 octets par defaut +``` + +- **`INPUT_PULLUP` avant `sensors.begin()`** : le tirage interne doit + être actif avant le premier échange 1-Wire (voir chapitre Arduino 1). +- **`setWaitForConversion(false)`** : sinon `requestTemperatures()` + attend la fin de la mesure (750 ms à 12 bits). On gère cette attente + soi-même, sans bloquer. +- **`esp_read_mac()`** : l'identifiant (`xiao_ds18b20_1a2b3c`) dérive + de la MAC. `WiFi.macAddress()` renverrait ici des zéros, la station + n'étant pas démarrée : toutes les cartes auraient le même identifiant. + `esp_read_mac()` lit l'adresse gravée en usine (l'eFuse). +- **`WiFi.begin()`** rend la main aussitôt, sans boucle + `while (WiFi.status() != WL_CONNECTED)` : `loop()` suivra le réseau. +- **`setBufferSize(1024)`** : PubSubClient limite un paquet à 256 + octets, or le JSON de découverte (voir chapitre Arduino 3) en fait 533. + +## loop() : une boucle qui ne bloque jamais + +Un sketch typique de l'IDE ressemble à ceci (exemple schématique, qui ne +vient pas du projet) : + +```cpp +void loop() { + sensors.requestTemperatures(); // attend 750 ms + float c = sensors.getTempCByIndex(0); + client.publish("maison/temperature", String(c).c_str()); + delay(10000); // attend 10 s +} +``` + +Pendant `delay(10000)`, le programme ne fait **rien d'autre** : il ne +voit pas la perte du WiFi et ne répond pas au broker, qui finit par le +déconnecter. La boucle du projet (lignes 198 à 203) est à l'opposé : + +```cpp +void loop() { + handleWifi(); + handleMqtt(); + handleSensor(); + delay(10); +} +``` + +Chaque `handle*()` regarde l'heure, décide s'il y a quelque chose à +faire, le fait vite et **rend la main** : chaque tâche est examinée +environ cent fois par seconde. Le motif qui remplace `delay()`, avec +`millis()` (millisecondes depuis le démarrage, en `uint32_t`) : + +```cpp +if (millis() - last < INTERVAL) return; // pas encore l'heure +last = millis(); +// ... faire le travail ... +``` + +## Le débordement de millis() + +Un `uint32_t` va de 0 à 4 294 967 295 : le compteur repasse à 0 au bout +de 2^32 ms, soit **49,71 jours**, ce qui arrivera sur une sonde branchée +des mois. + +La soustraction de deux `uint32_t` se calcule **modulo 2^32** : un +résultat « négatif » reçoit 2^32 en plus. `now - last` donne donc +toujours la durée écoulée, même si `now` est repassé par zéro. Avec +`last = 4 294 962 296` (5 s avant le débordement) et un intervalle de +10 000 ms : + +| `now` | `now - last` | Action | +|-------|--------------|--------| +| 4 294 967 295 | 4 999 | attendre | +| 0 (débordement) | 5 000 | attendre | +| 5 000 | 10 000 | agir | + +La tâche se déclenche exactement 10 s après la précédente. L'écriture +naïve `if (now >= last + INTERVAL)` échoue : `last + INTERVAL` déborde +aussi et vaut 5 000, si bien que la condition est vraie à chaque tour +de boucle jusqu'au débordement de `now`. Règle : **toujours soustraire, +jamais additionner**, avec des opérandes en `uint32_t`. + +## handleSensor() : une conversion en deux temps + +Une mesure DS18B20 demande une conversion, lue 750 ms plus tard. +`handleSensor()` (lignes 141 à 166) découpe l'attente avec `converting` : + +```cpp + if (!converting) { + if (now - lastRead < READ_INTERVAL_MS && lastRead != 0) return; + sensors.requestTemperatures(); + converting = true; + conversionStart = now; + return; + } + if (now - conversionStart < sensors.millisToWaitForConversion()) return; + converting = false; +``` + +C'est une **machine à deux états** : + +``` + 10 s écoulées : requestTemperatures() + +----------+ ------------------------------> +------------+ + | au repos | | conversion | + +----------+ <------------------------------ +------------+ + 750 ms écoulées : lire, valider, publier +``` + +Pendant la conversion, la boucle continue de servir le WiFi et MQTT. +`lastRead != 0` déclenche la première mesure dès le démarrage. + +## Valider une lecture + +`isValidReading` (`temp_logic.cpp`, lignes 78 à 85) écarte les valeurs +fausses. Les constantes valent -127, -55, 125, 85 et 10 (tolérance) : + +```cpp +bool isValidReading(float celsius, bool hasPrevious, float previous) { + if (celsius == kDisconnected) return false; + if (!(celsius >= kMin && celsius <= kMax)) return false; // rejette aussi NaN + if (celsius == kPowerOn) { + return hasPrevious && fabsf(previous - kPowerOn) <= kPowerOnTolerance; + } + return true; +} +``` + +- **-127 °C** : sonde muette ou somme de contrôle (CRC) fausse. +- **Hors de -55 à 125 °C** (plage de la DS18B20) : la forme `!(...)` + rejette aussi NaN, car toute comparaison avec NaN est fausse. +- **85 °C** : valeur de mise sous tension, suspecte sauf si la lecture + valide précédente était proche (75 à 95 °C). + +`==` sur des `float` est correct ici : -127 et 85 sont renvoyés +exactement. Quand la sonde passe hors ligne, `handleSensor()` oublie +`previous` (ligne 159) : rebranchée, elle renverra 85, alors rejeté. + +## PublishPolicy et ProbeHealth + +Publier toutes les 10 s remplirait l'historique de Home Assistant de +valeurs identiques. `PublishPolicy` (`temp_logic.cpp`, lignes 96 à 100) +décide : + +```cpp +bool PublishPolicy::shouldPublish(int tenths, uint32_t nowMs) const { + if (!hasPublished_) return true; + if (abs(tenths - lastTenths_) >= minDelta_) return true; + return (uint32_t)(nowMs - lastMs_) >= maxInterval_; +} +``` + +Par défaut (2 dixièmes, 300 000 ms), on publie la première valeur, puis +à chaque écart d'au moins 0,2 °C, et au moins toutes les 5 minutes (un +« battement de coeur »). `reset()`, appelé à chaque connexion MQTT, fait +partir la valeur suivante. Les **dixièmes entiers** (`toTenths(21.4375)` +vaut 214) se comparent exactement, contrairement aux `float` (voir +« Pour aller plus loin »). + +`ProbeHealth` (lignes 108 à 117) suit l'état de la sonde : + +```cpp + } else if (failures_ < maxFailures_ && ++failures_ >= maxFailures_) { + online_ = false; + } + return online_ != before; +``` + +La sonde passe hors ligne après **3 échecs d'affilée** (une lecture +ratée isolée ne compte pas) et revient dès la première lecture valide. +`failures_ < maxFailures_` empêche le compteur (`uint8_t`) de déborder. +`record()` ne renvoie `true` qu'au **changement** d'état, signal pour +publier sur le sujet `probe` (voir chapitre Arduino 3). + +## WiFi et MQTT : se reconnecter sans bloquer + +Le core ESP32 reconnecte le WiFi tout seul, mais abandonne dans certains +cas (une box qui redémarre et refuse l'authentification). `handleWifi()` +(lignes 82 à 101) relance donc toutes les 30 s : + +```cpp + lastKick = millis(); // laisser d'abord le core se reconnecter +... + if (connected) return; + if (millis() - lastKick < WIFI_KICK_INTERVAL_MS) return; + lastKick = millis(); +... + WiFi.reconnect(); // non bloquant +``` + +`lastKick` est `static` : il garde sa valeur d'un appel à l'autre. Sa +remise à l'heure à la perte du réseau laisse 30 s au core. + +`handleMqtt()` (lignes 103 à 128) fait vivre la connexion au broker : + +```cpp + static uint32_t lastAttempt = 0 - RETRY_INTERVAL_MS; // 1re tentative immediate +... + if (mqtt.connected()) { + mqtt.loop(); +... + if (millis() - lastAttempt < RETRY_INTERVAL_MS) return; + const bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS, topics.status, 0, true, "offline"); + lastAttempt = millis(); // delai compte depuis la fin de la tentative +``` + +- **`0 - RETRY_INTERVAL_MS`** vaut 4 294 962 296. Au premier appel, + `millis() - lastAttempt` vaut `millis() + 5000`, toujours au moins + 5 000 : la première tentative est immédiate. +- **`lastAttempt = millis()` après `connect()`** : `connect()`, seule + opération bloquante, dure au plus environ 8 s (3 s de TCP plus 5 s de + réponse, réglés aux lignes 191 et 193 de `setup()`). En comptant + depuis la **fin** de la tentative, on garantit 5 s de boucle normale + entre deux essais, même si le broker ne répond jamais. +- **`mqtt.loop()`** lit les messages et entretient la connexion : il + doit être appelé souvent, d'où l'interdiction de bloquer. + +Les messages reçus arrivent dans le rappel `onMqttMessage()` (lignes 72 +à 77) : + +```cpp + if (strcmp(topic, HA_STATUS_TOPIC) == 0 && length == 6 && + memcmp(payload, "online", 6) == 0) { + discoveryRequested = true; // Home Assistant a redemarre + } +``` + +`payload` n'est pas terminé par un zéro : on compare la longueur, puis +les octets. Le rappel ne publie rien : il lève un **drapeau** que +`handleMqtt()` traite au tour suivant, hors de `mqtt.loop()`. + +## L'essentiel + +| Commande / Notion | Usage | +|-------------------|-------| +| `src/` contre `lib/` | Code matériel mince d'un côté, logique pure testable sur PC de l'autre | +| `namespace temp` | Regroupe la logique, évite les collisions de noms | +| `credentials.h` | Secrets hors dépôt (`.gitignore`), modèle `credentials.h.example` versionné | +| `#ifndef ... #error` | Arrêter la compilation avec un message clair si un secret manque | +| `esp_read_mac()` | MAC lue dans l'eFuse, disponible avant le démarrage du WiFi | +| `setBufferSize(1024)` | Agrandir le tampon de PubSubClient (256 octets par défaut) | +| `loop()` non bloquante | Des `handle*()` qui décident vite et rendent la main | +| `millis() - last < INTERVAL` | Motif d'attente sans `delay()`, sûr au débordement (49,71 jours) | +| `setWaitForConversion(false)` | Conversion DS18B20 asynchrone (750 ms à 12 bits) | +| `isValidReading` | Rejette -127, NaN, hors -55..125, et 85 sans précédent proche | +| `PublishPolicy` | Publier si écart >= 0,2 °C ou toutes les 5 min ; `reset()` à la reconnexion | +| `toTenths` | Comparer des dixièmes entiers plutôt que des `float` | +| `ProbeHealth` | Hors ligne après 3 échecs d'affilée, en ligne au premier succès | +| `0 - RETRY_INTERVAL_MS` | Première tentative immédiate grâce à l'arithmétique non signée | +| Rappel + drapeau | Le rappel MQTT note l'événement, la boucle le traite | + +## Pour aller plus loin + +**Le piège des `float`.** Sur PC, `21.3f - 21.1f` vaut 0,199998856 +(l'ESP32 utilise le même format IEEE 754 pour les `float`) : le test +`>= 0.2f` serait faux, alors que l'affichage montre bien 0,2 °C d'écart. +En dixièmes entiers, `213 - 211 = 2`, sans ambiguïté. C'est la raison +de `toTenths` dans `PublishPolicy`. + +**Le piège des petits entiers.** La soustraction modulo ne fonctionne +que si le calcul reste dans le type non signé. Avec des `uint16_t`, le +C++ convertit d'abord les opérandes en `int` (promotion entière) : sur +un ESP32 ou un PC, où `int` fait 32 bits, `100 - 65000` vaut -64 900 et +non 636. Il faudrait écrire `(uint16_t)(a - b)`. C'est pour la même +raison que `shouldPublish` convertit explicitement +`(uint32_t)(nowMs - lastMs_)` : le code reste juste quelle que soit la +taille d'`int` sur la plateforme. Avec `uint32_t` partout, comme ici, le +piège ne se présente pas. + +**Le cas `lastRead == 0`.** Dans `handleSensor()`, la condition +`lastRead != 0` sert au démarrage. Si, après un débordement, `millis()` +valait exactement 0 au moment d'une lecture, la lecture suivante +partirait sans attendre 10 s. La conséquence est une mesure en avance, +une fois tous les 49 jours au pire : l'auteur a jugé ce cas acceptable. +`handleMqtt()` montre l'autre solution, sans cas particulier : +initialiser à `0 - INTERVALLE`. + +**`loop()`, `delay(10)` et FreeRTOS.** Sur l'ESP32, le framework +Arduino s'appuie sur le système temps réel FreeRTOS : `loop()` est +appelée en boucle par une tâche, et la pile WiFi tourne dans d'autres +tâches. Le `delay(10)` en fin de `loop()` met la tâche en sommeil et +laisse le processeur aux autres. + +**Pourquoi pas de `String`.** Le programme manipule des tableaux de +`char` de taille fixe (`deviceId[32]`, `payload[768]`) et `snprintf`, +jamais la classe `String` d'Arduino. Sur un microcontrôleur qui tourne +des mois, les allocations dynamiques répétées fragmentent la mémoire. +Avec des tampons fixes, la mémoire utilisée est connue à la compilation. + +**Ce qui reste bloquant.** La résolution DNS du nom du broker (et mDNS +pour un nom en `.local`) n'est pas couverte par les délais d'attente de +`setup()`. Pour un appareil plus exigeant, on placerait la connexion +MQTT dans une tâche FreeRTOS séparée, ou on utiliserait une bibliothèque +MQTT asynchrone. + +## Exercices + +Aucun exercice ne demande la carte. L'exercice 1 se vérifie avec +`pio run` sur un projet PlatformIO vide (voir chapitre Arduino 2). + +**Exercice 1.** Ce sketch fait clignoter la LED et affiche une mesure +analogique, mais la LED ne change d'état que toutes les 10 s : + +```cpp +#include + +void setup() { + Serial.begin(115200); + pinMode(LED_BUILTIN, OUTPUT); +} + +void loop() { + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + Serial.println(analogRead(A0)); + delay(10000); +} +``` + +Réécrivez-le sans `delay()` long : la LED change d'état toutes les +500 ms et la mesure s'affiche toutes les 10 s, la première dès le +démarrage. Suivez le style du projet (une fonction `handle*()` par +tâche). + +**Exercice 2.** Une lecture a eu lieu à `lastRead = 4 294 965 000`. +L'intervalle est de 10 000 ms. Calculez `millis() - lastRead` pour +`millis() = 3 000`, puis `millis() = 9 000`. Dans chaque cas, la +lecture suivante a-t-elle lieu ? + +**Exercice 3.** Pour chaque appel, dites ce que renvoie +`isValidReading(celsius, hasPrevious, previous)` et pourquoi : + +1. `(21.5, false, 0)` +2. `(-127, true, 20)` +3. `(85, false, 0)` +4. `(85, true, 21.5)` +5. `(85, true, 80)` +6. `(125.5, true, 20)` +7. `(84.9375, false, 0)` + +**Exercice 4.** Une `PublishPolicy` par défaut reçoit, toutes les 10 s +à partir de `t = 0`, les valeurs en dixièmes 214, 215, 216, 216, 213. +Lesquelles sont publiées ? Si la température reste ensuite à 213, à +quel instant la publication suivante a-t-elle lieu ? + +**Exercice 5.** Justifiez pourquoi `PublishPolicy` est dans `lib/` alors +que `handleWifi()` est dans `src/main.cpp`. Pourquoi `shouldPublish` +reçoit-elle `nowMs` en paramètre au lieu d'appeler `millis()` ? + +### Solutions + +**Solution 1.** Deux tâches, chacune avec son propre instant de +référence : + +```cpp +#include + +static const uint32_t BLINK_INTERVAL_MS = 500; +static const uint32_t READ_INTERVAL_MS = 10000; + +static void handleLed() { + static uint32_t lastToggle = 0; + if (millis() - lastToggle < BLINK_INTERVAL_MS) return; + lastToggle = millis(); + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); +} + +static void handleRead() { + static uint32_t lastRead = 0 - READ_INTERVAL_MS; // 1re lecture immediate + if (millis() - lastRead < READ_INTERVAL_MS) return; + lastRead = millis(); + Serial.println(analogRead(A0)); +} + +void setup() { + Serial.begin(115200); + pinMode(LED_BUILTIN, OUTPUT); +} + +void loop() { + handleLed(); + handleRead(); + delay(10); +} +``` + +Les deux versions compilent avec `pio run` pour `seeed_xiao_esp32s3`. +L'initialisation `0 - READ_INTERVAL_MS` reprend l'astuce de +`handleMqtt()`. Ajouter une troisième tâche ne ralentit pas les deux +premières. + +**Solution 2.** Le compteur a débordé entre les deux instants. Il +restait 4 294 967 296 - 4 294 965 000 = 2 296 ms avant le débordement : + +- `3 000 - 4 294 965 000` = 2 296 + 3 000 = **5 296** : inférieur à + 10 000, on attend ; +- `9 000 - 4 294 965 000` = 2 296 + 9 000 = **11 296** : la lecture a + lieu. + +**Solution 3.** + +1. `true` : valeur normale, dans la plage. +2. `false` : -127, sonde absente ou CRC faux. +3. `false` : 85 sans lecture précédente, probablement la valeur de mise + sous tension. +4. `false` : 85 alors que la précédente valait 21,5 ; l'écart (63,5) + dépasse la tolérance de 10. +5. `true` : 85 après 80, écart de 5, le radiateur chauffe vraiment. +6. `false` : hors de la plage -55..125. +7. `true` : seul 85 exactement est suspect ; 84,9375 est une valeur + normale (pas de 0,0625 °C à 12 bits). + +**Solution 4.** À 0 s, 214 est publié (première valeur). À 10 s, 215 : +écart de 1 avec 214, rien. À 20 s, 216 : écart de 2, publié. À 30 s, +216 : écart nul, rien. À 40 s, 213 : écart de 3 avec 216, publié. La +température restant à 213, la publication suivante est le battement de +coeur, 5 minutes après la dernière : à 340 s (40 + 300). + +**Solution 5.** `PublishPolicy` est de la logique pure : elle ne prend +que des entiers et ne touche à aucun matériel. Elle compile sur PC et se +teste avec des cas choisis. `handleWifi()` appelle `WiFi.status()` et +`WiFi.reconnect()`, qui n'existent que sur l'ESP32 : elle ne peut vivre +que dans `src/`. `shouldPublish` reçoit l'heure en paramètre pour rester +indépendante de `millis()` : un test peut lui passer n'importe quel +instant (5 minutes plus tard, juste avant le débordement...) sans +attendre réellement. From a3986fdcf0027378996bf5e6a8fa2d496e6a05fa Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:13:39 +0200 Subject: [PATCH 08/12] docs: chapitre Arduino 5 (tests unitaires) Co-Authored-By: Claude Opus 5.5 --- .../module_arduino/05_tests_unitaires.md | 508 +++++++++++++++++- 1 file changed, 506 insertions(+), 2 deletions(-) diff --git a/supports/modules_additionnels/module_arduino/05_tests_unitaires.md b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md index 245d4c6..2e46910 100644 --- a/supports/modules_additionnels/module_arduino/05_tests_unitaires.md +++ b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md @@ -1,5 +1,509 @@ # Arduino 5 — Les tests unitaires -> **Objectifs** : à rédiger. +> **Objectifs** : à la fin de ce chapitre, vous saurez : +> - expliquer pourquoi on teste du code embarqué, et pourquoi on le fait sur le PC ; +> - lire une suite de tests Unity (`setUp`, `TEST_ASSERT_*`, `RUN_TEST`) ; +> - lancer les tests avec `pio test -e native` et lire un échec ; +> - écrire un nouveau test pour un cas limite, et vérifier qu'il sait échouer. > -> **Durée de lecture** : à préciser. +> **Durée de lecture** : environ 1 h 15, exercices compris. Aucune carte n'est nécessaire. + +--- + +Avec l'IDE Arduino, on vérifie un programme en le téléversant sur la +carte et en regardant le moniteur série. Cette méthode a ses limites : +elle est lente, elle demande le matériel, et elle ne montre que les +situations qui se présentent pendant l'essai. + +Ce chapitre présente une autre méthode : les **tests unitaires**. De +petits programmes appellent les fonctions du projet avec des valeurs +choisies et vérifient les résultats. Ils tournent sur votre PC, en +quelques secondes, sans carte. Le projet `DS18B20_MQTT` en contient 24. + +## Pourquoi tester du code embarqué + +Un bogue dans la logique d'un capteur se voit rarement tout de suite. +Trois exemples tirés du projet : + +- **L'arrondi.** La sonde renvoie 21,45 °C ; le programme publie des + dixièmes de degré. Faut-il publier 21,4 ou 21,5 ? Une conversion + maladroite tronque au lieu d'arrondir, et l'erreur passe inaperçue + sur un affichage. +- **Les cas limites.** Le DS18B20 renvoie 85 °C à la mise sous tension + (voir chapitre Arduino 4). Le programme doit rejeter cette valeur, + sauf si la température était déjà proche de 85 °C. Pour vérifier cela + sur la carte, il faudrait chauffer la sonde à 80 °C. +- **Le débordement.** `millis()` revient à zéro au bout d'environ + 49,7 jours (voir chapitre Arduino 4). Personne n'attend sept semaines + devant son moniteur série pour vérifier que le heartbeat survit. + +S'ajoutent les **régressions** : une modification qui casse ce qui +marchait. En relançant les tests après chaque modification, on les voit +immédiatement. Un test unitaire ne remplace pas l'essai sur la carte : +il le complète, en couvrant les cas rares, rapidement et à chaque fois. + +## Tester sur le PC : l'environnement `native` + +Le fichier `platformio.ini` du projet déclare deux environnements (voir +chapitre Arduino 2). Le second sert aux tests : + +```ini +[env:native] +platform = native +build_flags = -std=c++17 -Wall -Wextra +lib_deps = + bblanchon/ArduinoJson@^7.0.0 +``` + +- `platform = native` : le code est compilé pour votre PC, avec le + compilateur du système (`g++` sous Linux), et non pour l'ESP32-S3. +- `build_flags` : la norme C++17 et les avertissements (`-Wall + -Wextra`) ; un avertissement signale souvent un vrai problème. +- `lib_deps` : seulement ArduinoJson, qui fonctionne aussi hors + Arduino. Ni OneWire, ni DallasTemperature, ni PubSubClient : ces + bibliothèques parlent au matériel et ne compilent pas sur un PC. + +Cela n'est possible que grâce au découpage du projet (voir chapitre +Arduino 4) : `src/main.cpp` contient tout ce qui touche au matériel, +`lib/temp_logic/` la logique pure, sans `#include `. Lors +d'un test, PlatformIO ne compile pas `src/` : il compile la suite de +tests et les bibliothèques de `lib/` qu'elle inclut. + +On ne teste pas ainsi la lecture réelle sur le bus 1-Wire, le WiFi, le +broker ni le câblage. En revanche, toute la logique de décision +(validation, arrondi, publication, santé de la sonde, JSON de +découverte) est vérifiée en quelques secondes, sur n'importe quel PC et +sur le serveur d'intégration continue (voir chapitre Arduino 6). + +## Unity, le cadre de test + +PlatformIO utilise par défaut **Unity**, un cadre de test en C très +léger, conçu pour l'embarqué. Le projet ne le déclare pas : au premier +`pio test`, PlatformIO l'installe tout seul +(`throwtheswitch/Unity @ ^2.6.1`). + +Une suite Unity est un programme C++ ordinaire, avec une fonction +`main()` : + +| Élément | Rôle | +|---------|------| +| `#include ` | Déclare les macros de Unity | +| `void setUp()` | Appelée avant chaque test (préparation commune) | +| `void tearDown()` | Appelée après chaque test (nettoyage) | +| `void test_xxx()` | Un test : une fonction sans paramètre | +| `TEST_ASSERT_TRUE(x)` / `TEST_ASSERT_FALSE(x)` | Vérifie qu'une condition est vraie / fausse | +| `TEST_ASSERT_EQUAL_INT(attendu, obtenu)` | Compare deux entiers | +| `TEST_ASSERT_EQUAL_STRING(attendu, obtenu)` | Compare deux chaînes C | +| `UNITY_BEGIN()` | Démarre la suite | +| `RUN_TEST(test_xxx)` | Exécute un test et note son résultat | +| `UNITY_END()` | Affiche le bilan et renvoie le nombre d'échecs | + +Dès qu'une assertion échoue, Unity abandonne le test en cours, note +l'échec et passe au test suivant. L'ordre des arguments compte : la +valeur **attendue** d'abord, la valeur **obtenue** ensuite. Le message +d'échec en dépend. Dans le projet, `setUp` et `tearDown` sont vides +(chaque test crée ses propres objets), mais Unity exige leur présence. + +PlatformIO range les tests dans `test/`, **une suite par dossier** +dont le nom commence par `test_` : + +``` +test/ ++-- test_reading/test_main.cpp validation, arrondi, formatage (8 tests) ++-- test_publish/test_main.cpp PublishPolicy (6 tests) ++-- test_probe/test_main.cpp ProbeHealth (6 tests) ++-- test_discovery/test_main.cpp sujets et JSON de découverte (4 tests) +``` + +Chaque dossier est compilé en un programme séparé, puis exécuté. + +## Lecture d'une suite : `test_reading` + +Voici l'essentiel de `test/test_reading/test_main.cpp` +([source](https://github.com/pguinet/arduino/blob/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT/test/test_reading/test_main.cpp)) : + +```cpp +#include +#include +#include "temp_logic.h" + +using namespace temp; + +void setUp() {} +void tearDown() {} +... +void test_real_85_accepted_when_previous_is_close() { + TEST_ASSERT_TRUE(isValidReading(85.0f, true, 84.5f)); + TEST_ASSERT_TRUE(isValidReading(85.0f, true, 75.0f)); + TEST_ASSERT_FALSE(isValidReading(85.0f, true, 74.9f)); +} +... +void test_to_tenths_rounds_half_away_from_zero() { + TEST_ASSERT_EQUAL_INT(214, toTenths(21.4375f)); + TEST_ASSERT_EQUAL_INT(215, toTenths(21.45f)); + TEST_ASSERT_EQUAL_INT(0, toTenths(0.04f)); + TEST_ASSERT_EQUAL_INT(-31, toTenths(-3.0625f)); + TEST_ASSERT_EQUAL_INT(-5, toTenths(-0.5f)); +} + +void test_format_tenths() { + char b[8]; + formatTenths(214, b, sizeof b); TEST_ASSERT_EQUAL_STRING("21.4", b); + formatTenths(0, b, sizeof b); TEST_ASSERT_EQUAL_STRING("0.0", b); + formatTenths(-5, b, sizeof b); TEST_ASSERT_EQUAL_STRING("-0.5", b); + formatTenths(-31, b, sizeof b); TEST_ASSERT_EQUAL_STRING("-3.1", b); + formatTenths(1250, b, sizeof b); TEST_ASSERT_EQUAL_STRING("125.0", b); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_disconnected_is_rejected); + ... + RUN_TEST(test_format_tenths); + return UNITY_END(); +} +``` + +Quelques remarques : + +- **Les noms des tests sont une documentation.** + `test_real_85_accepted_when_previous_is_close` se lit comme une + règle : « un vrai 85 est accepté quand la lecture précédente est + proche ». La liste des tests résume le comportement attendu. +- **Les valeurs sont choisies aux frontières.** La tolérance autour de + 85 °C est de 10 degrés : le test essaie 75,0 (accepté, juste dans la + limite) et 74,9 (rejeté, juste au-delà). Un bogue de frontière, `<` + au lieu de `<=`, serait détecté. +- **Les valeurs viennent du capteur réel** : 21,4375 et -3,0625 sont + des multiples de 1/16 de degré, la résolution du DS18B20. +- **Les cas pièges sont explicites.** `-0.5` est un piège classique : + la partie entière de -5 / 10 vaut 0, et un formatage naïf perdrait + le signe moins. + +## Lancer les tests + +Depuis le dossier du projet : + +``` +$ pio test -e native +... +Collected 4 tests + +Processing test_reading in native environment +... +test/test_reading/test_main.cpp:61: test_disconnected_is_rejected [PASSED] +... +test/test_reading/test_main.cpp:68: test_format_tenths [PASSED] +---------------- native:test_reading [PASSED] Took 4.98 seconds ---------------- +... +native test_discovery PASSED 00:00:03.573 +================= 24 test cases: 24 succeeded in 00:00:11.395 ================= +``` + +« Collected 4 tests » compte les **suites** (les dossiers) ; le bilan +final compte les **cas de test** (les fonctions) : 24. Chaque ligne +indique le fichier, le numéro de la ligne `RUN_TEST`, le nom du test +et son résultat. + +Deux options servent souvent : + +- `-f test_reading` (*filter*) : ne lance que la suite indiquée. Pratique + quand on travaille sur une seule partie du code. +- `-v` : affiche la sortie brute de Unity (`8 Tests 0 Failures 0 + Ignored`, puis `OK`). + +L'option `-e native` est indispensable : sans elle, PlatformIO prend +l'environnement par défaut, `xiao`, et chercherait une carte branchée. + +## Lire un échec + +Pour voir un échec, cassez volontairement `toTenths`, dans +`lib/temp_logic/temp_logic.cpp`. Remplacez l'arrondi par une simple +conversion en entier, qui tronque : + +```cpp +int toTenths(float celsius) { + return (int)(celsius * 10.0f); // au lieu de (int)lroundf(celsius * 10.0f) +} +``` + +Puis relancez la suite concernée : + +``` +$ pio test -e native -f test_reading +... +test/test_reading/test_main.cpp:66: test_ordinary_readings_accepted [PASSED] +test/test_reading/test_main.cpp:44: test_to_tenths_rounds_half_away_from_zero: Expected 215 Was 214 [FAILED] +test/test_reading/test_main.cpp:68: test_format_tenths [PASSED] +Program received signal SIGHUP (Hangup) +--------------- native:test_reading [ERRORED] Took 1.42 seconds --------------- +... +============= 9 test cases: 1 failed, 7 succeeded in 00:00:01.422 ============= +``` + +La ligne utile est celle marquée `[FAILED]` : + +- `test_main.cpp:44` : pour un échec, le numéro est celui de + l'**assertion** qui a échoué, et non plus celui du `RUN_TEST`. La + ligne 44 est `TEST_ASSERT_EQUAL_INT(215, toTenths(21.45f));`. +- `Expected 215 Was 214` : on attendait 215, on a obtenu 214. Tronquer + 214,5 donne 214 ; l'arrondi donnait 215. + +Le reste est trompeur. `UNITY_END()` renvoie le nombre d'échecs (ici 1) +comme code de retour du programme ; PlatformIO prend ce code pour un +numéro de signal, et le signal 1 se nomme SIGHUP. Aucun signal n'a été +envoyé : avec deux échecs, vous liriez `SIGINT` (signal 2). D'où aussi +l'état `ERRORED` et le neuvième « cas » du bilan. Retenez : `[FAILED]` +désigne le test fautif, et `pio test` se termine avec le code 1, ce qui +suffit à faire échouer l'intégration continue (voir chapitre Arduino 6). + +Annulez la modification avant de continuer (voir module additionnel +Git) : + +``` +$ git restore lib/temp_logic/temp_logic.cpp +``` + +## Écrire un test + +Pour une fonction donnée, cherchez trois familles de cas : + +1. **le cas nominal** : une valeur ordinaire (21,4 °C) ; +2. **les limites** : les frontières des intervalles (-55, 125, la + tolérance de 10 degrés, l'intervalle de 300000 ms) ; +3. **les erreurs** : les valeurs que le capteur ne devrait pas renvoyer + mais renvoie quand même (-127, 85, NaN). + +Prenons `PublishPolicy` (voir chapitre Arduino 4). La suite +`test_publish` vérifie déjà le heartbeat à 299999 ms et à exactement +300000 ms après une publication. Un cas manque : après une deuxième +publication, le délai doit repartir de **cette** publication, et non de +la première. Ajoutez ce test dans `test/test_publish/test_main.cpp`, +avant `main()` : + +```cpp +void test_heartbeat_restarts_from_last_publish() { + PublishPolicy p; + p.markPublished(214, 1000); + p.markPublished(214, 301000); // publication du heartbeat + TEST_ASSERT_FALSE(p.shouldPublish(214, 301000 + 299999)); + TEST_ASSERT_TRUE(p.shouldPublish(214, 301000 + 300000)); +} +``` + +Puis ajoutez `RUN_TEST(test_heartbeat_restarts_from_last_publish);` +dans `main()`, après le dernier `RUN_TEST` : un test oublié là n'est +jamais exécuté, sans aucun message. Lancez la suite : + +``` +$ pio test -e native -f test_publish +... +test/test_publish/test_main.cpp:64: test_reset_forces_next_publish [PASSED] +test/test_publish/test_main.cpp:65: test_heartbeat_restarts_from_last_publish [PASSED] +---------------- native:test_publish [PASSED] Took 0.70 seconds ---------------- +... +================== 7 test cases: 7 succeeded in 00:00:00.705 ================== +``` + +Un test qui passe du premier coup ne prouve pas grand-chose : il faut +vérifier qu'il **sait échouer**. Introduisez un bogue plausible dans +`markPublished` : ne mémoriser l'heure que lors de la toute première +publication. + +```cpp +void PublishPolicy::markPublished(int tenths, uint32_t nowMs) { + if (!hasPublished_) lastMs_ = nowMs; + hasPublished_ = true; + lastTenths_ = tenths; +} +``` + +Les six tests d'origine passent toujours : aucun n'appelle +`markPublished` deux fois. Le nouveau test, lui, détecte le bogue : + +``` +test/test_publish/test_main.cpp:64: test_reset_forces_next_publish [PASSED] +test/test_publish/test_main.cpp:53: test_heartbeat_restarts_from_last_publish: Expected FALSE Was TRUE [FAILED] +``` + +C'est exactement ce qu'on attend d'un nouveau test : il couvre un cas +que la suite ne voyait pas. Annulez le bogue avec `git restore`, et +gardez le test. + +## L'essentiel + +| Commande / Notion | Usage | +|-------------------|-------| +| Test unitaire | Petit programme qui appelle une fonction et vérifie son résultat | +| Régression | Ce qui marchait et ne marche plus après une modification | +| `[env:native]` | Environnement qui compile pour le PC, sans le matériel | +| Unity | Cadre de test par défaut de PlatformIO | +| `setUp()` / `tearDown()` | Appelées avant / après chaque test (obligatoires, même vides) | +| `TEST_ASSERT_TRUE` / `_FALSE` | Vérifier une condition | +| `TEST_ASSERT_EQUAL_INT` / `_STRING` | Comparer : valeur attendue d'abord, obtenue ensuite | +| `UNITY_BEGIN()`, `RUN_TEST()`, `UNITY_END()` | Démarrer, exécuter un test, faire le bilan | +| `test/test_xxx/` | Un dossier = une suite = un programme | +| `pio test -e native` | Lancer toutes les suites sur le PC | +| `pio test -e native -f test_xxx` | Lancer une seule suite | +| `pio test ... -v` | Afficher la sortie brute de Unity | +| `[FAILED]` | Fichier, ligne de l'assertion, valeurs attendue et obtenue | +| Cas nominal, limites, erreurs | Les trois familles de cas à tester | + +## Pour aller plus loin + +**Les autres assertions.** Unity en propose beaucoup d'autres. Le +projet utilise aussi `TEST_ASSERT_EQUAL` (entier générique) et +`TEST_ASSERT_GREATER_THAN` dans `test_discovery`. Pour les nombres à +virgule, `TEST_ASSERT_FLOAT_WITHIN(delta, attendu, obtenu)` tolère un +écart : comparer deux `float` avec `==` est presque toujours une +erreur, à cause des arrondis binaires. Le projet contourne la question +en testant des dixièmes entiers. + +**Tester sur la carte.** PlatformIO sait aussi exécuter une suite sur +la carte : `pio test -e xiao` compile les tests pour l'ESP32-S3, les +téléverse et lit les résultats sur le port série. C'est utile pour du +code qui dépend du matériel. Le projet ne le fait pas : sa logique est +entièrement testable sur le PC, et la CI n'a pas de carte. + +**Les doublures.** Pour tester du code qui appelle le matériel (lire la +sonde, publier sur MQTT), on le fait dépendre d'une interface, puis on +fournit au test une fausse implémentation (*mock* ou *fake*) qui +renvoie des valeurs choisies. C'est plus de travail ; le découpage du +projet évite d'en avoir besoin. + +**Le développement piloté par les tests.** La démarche TDD (*Test +Driven Development*) inverse l'ordre : on écrit d'abord un test qui +échoue, puis le code minimal qui le fait passer, puis on nettoie. +L'exemple de la section « Écrire un test » en reprend l'idée clé : un +test ne vaut que s'il a été vu échouer. + +**La couverture.** Des outils comme `gcov` mesurent les lignes de code +exécutées par les tests. Une couverture de 100 % ne garantit pas +l'absence de bogue (le bogue de `markPublished` ci-dessus passait les +24 tests, alors que toutes ses lignes étaient exécutées), mais une +ligne jamais exécutée n'est jamais vérifiée. + +**Documentation.** La page « Unit Testing » de la documentation de +PlatformIO (https://docs.platformio.org/en/latest/advanced/unit-testing/) +détaille les options de `pio test` et les autres cadres de test +disponibles (GoogleTest, doctest). + +## Exercices + +Tous les exercices se font sans carte, dans votre clone du dépôt +`arduino`, dossier `sketches/XIAO-ESP32-S3/DS18B20_MQTT`. Annulez vos +modifications avec `git restore` à la fin de chaque exercice. + +**Exercice 1.** Lancez `pio test -e native`. Combien de suites et +combien de cas de test sont exécutés ? Relancez seulement la suite +`test_probe` avec l'option `-v` : quelles lignes Unity affiche-t-il à +la fin ? + +**Exercice 2.** Dans `test_reading`, ajoutez deux tests : une lecture +de 85 °C après une lecture valide de 80 °C doit être acceptée ; une +lecture de 85 °C après 20 °C doit être rejetée. Vérifiez qu'ils +passent. Puis, dans `temp_logic.cpp`, réduisez la tolérance +`kPowerOnTolerance` de 10 à 2 degrés : quels tests échouent ? + +**Exercice 3.** Le cas `formatTenths(-5)` -> `"-0.5"` est-il déjà +testé ? Ajoutez un test pour deux autres valeurs négatives sensibles : +-1 (-0,1 °C) et -550 (-55,0 °C, la limite basse du DS18B20). + +**Exercice 4.** Dans `ProbeHealth::record`, mettez en commentaire la +ligne `failures_ = 0;`. Quel test échoue, et pourquoi ? Pourquoi +`test_recovers_after_offline` passe-t-il quand même ? + +**Exercice 5.** Proposez un cas que les 24 tests actuels ne couvrent +pas, et écrivez le test correspondant. + +### Solutions + +**Solution 1.** 4 suites (« Collected 4 tests ») et 24 cas de test +(« 24 test cases: 24 succeeded »). Avec `pio test -e native -f +test_probe -v`, Unity termine par : + +``` +----------------------- +6 Tests 0 Failures 0 Ignored +OK +``` + +**Solution 2.** Les deux tests, avant `main()` : + +```cpp +void test_85_after_80_accepted() { + TEST_ASSERT_TRUE(isValidReading(85.0f, true, 80.0f)); +} + +void test_85_after_20_rejected() { + TEST_ASSERT_FALSE(isValidReading(85.0f, true, 20.0f)); +} +``` + +et dans `main()` : + +```cpp + RUN_TEST(test_85_after_80_accepted); + RUN_TEST(test_85_after_20_rejected); +``` + +Avec une tolérance de 2 degrés, 80 n'est plus « proche » de 85 : + +``` +test/test_reading/test_main.cpp:22: test_real_85_accepted_when_previous_is_close: Expected TRUE Was FALSE [FAILED] +... +test/test_reading/test_main.cpp:60: test_85_after_80_accepted: Expected TRUE Was FALSE [FAILED] +test/test_reading/test_main.cpp:78: test_85_after_20_rejected [PASSED] +Program received signal SIGINT (Interrupt) +``` + +Le test existant échoue sur 75,0 (ligne 22), le vôtre sur 80. Le rejet +après 20 °C reste juste. Le signal 2, SIGINT, correspond aux deux +échecs (voir « Lire un échec »). + +**Solution 3.** Oui : `test_format_tenths` le vérifie déjà. Les deux +nouveaux cas : + +```cpp +void test_format_small_negatives() { + char b[8]; + formatTenths(-1, b, sizeof b); TEST_ASSERT_EQUAL_STRING("-0.1", b); + formatTenths(-550, b, sizeof b); TEST_ASSERT_EQUAL_STRING("-55.0", b); +} +``` + +sans oublier `RUN_TEST(test_format_small_negatives);`. Le test passe. +Le tampon de 8 caractères suffit : `"-55.0"` en occupe 6 avec le +caractère nul final. + +**Solution 4.** Seul `test_valid_reading_resets_failure_count` échoue : + +``` +test/test_probe/test_main.cpp:40: test_valid_reading_resets_failure_count: Expected TRUE Was FALSE [FAILED] +``` + +Sans la remise à zéro, les échecs s'accumulent malgré la lecture +valide intercalée : après deux échecs, une réussite laisse le compteur +à 2, et l'échec suivant le porte à 3. La sonde passe hors ligne alors +qu'elle n'a jamais échoué trois fois d'affilée. `test_recovers_after_offline` passe car il +ne vérifie que le retour en ligne, qui ne dépend pas du compteur : une +lecture valide met toujours `online_` à vrai. + +**Solution 5.** Plusieurs réponses sont possibles. Par exemple, aucun +test ne construit un `PublishPolicy` avec d'autres paramètres que ceux +par défaut (2 dixièmes, 300000 ms) : + +```cpp +void test_custom_parameters() { + PublishPolicy p(5, 60000); + p.markPublished(200, 0); + TEST_ASSERT_FALSE(p.shouldPublish(204, 1000)); + TEST_ASSERT_TRUE(p.shouldPublish(205, 1000)); + TEST_ASSERT_FALSE(p.shouldPublish(200, 59999)); + TEST_ASSERT_TRUE(p.shouldPublish(200, 60000)); +} +``` + +Autres pistes : le heartbeat après une deuxième publication (l'exemple +de la section « Écrire un test »), un `ProbeHealth(5)`, ou +`formatTenths` avec un tampon trop petit. From 0dfc9707f4c3a7e3d37f701e247bd73f8a932f4c Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:13:39 +0200 Subject: [PATCH 09/12] =?UTF-8?q?docs:=20chapitre=20Arduino=206=20(qualit?= =?UTF-8?q?=C3=A9=20et=20int=C3=A9gration=20continue)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../06_qualite_integration_continue.md | 579 +++++++++++++++++- 1 file changed, 577 insertions(+), 2 deletions(-) diff --git a/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md index eae60a6..facd1f9 100644 --- a/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md +++ b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md @@ -1,5 +1,580 @@ # Arduino 6 — Qualité et intégration continue -> **Objectifs** : à rédiger. +> **Objectifs** : à la fin de ce chapitre, vous saurez : +> - expliquer ce qu'un analyseur statique comme cppcheck trouve sans exécuter le code ; +> - lancer `pio check`, lire ses défauts et justifier une suppression en ligne ; +> - définir l'intégration continue et le vocabulaire de GitHub Actions (workflow, job, step, runner, action) ; +> - lire le fichier `ci.yml` du projet, ligne par ligne ; +> - retrouver dans l'onglet Actions le step qui a échoué et la ligne de log qui l'explique. > -> **Durée de lecture** : à préciser. +> **Durée de lecture** : ~1 h 30. + +--- + +Les tests unitaires vérifient que le code fait ce qu'on attend (voir +chapitre Arduino 5). Restent deux questions : le code contient-il des +maladresses que les tests n'exercent pas ? Et qui garantit que tests, +compilation et vérifications tournent à chaque modification ? La +première relève de l'**analyse statique**, la seconde de +l'**intégration continue**. Rien dans ce chapitre ne demande de carte. + +## L'analyse statique avec cppcheck + +Un **analyseur statique** lit le code sans l'exécuter. Il raisonne sur +les valeurs possibles des variables et l'usage des paramètres, et +signale ce qui ressemble à une erreur ou à une maladresse, même dans +une branche que les tests ne parcourent jamais. **cppcheck** est un +analyseur libre pour le C et le C++. PlatformIO l'intègre : `pio check` +le télécharge au besoin et lui passe les options de compilation de +l'environnement choisi (définitions `-D`, chemins d'inclusion). + +### La configuration dans platformio.ini + +L'environnement `xiao` du projet contient trois lignes consacrées à +l'analyse +([platformio.ini](https://github.com/pguinet/arduino/blob/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT/platformio.ini)) : + +```ini +check_tool = cppcheck +check_flags = cppcheck: --enable=warning,style,performance --inline-suppr --suppress=*:*/.pio/* +check_src_filters = + + +``` + +- `check_tool` choisit l'outil (PlatformIO propose aussi `clangtidy`). +- `check_flags` passe des options à cppcheck. `--enable=...` ajoute aux + erreurs, toujours actives, trois familles de contrôles : les + avertissements (code probablement faux), le style (code inutile ou + maladroit) et la performance (copie évitable). `--inline-suppr` + autorise les suppressions en ligne (voir plus bas). + `--suppress=*:*/.pio/*` ignore les défauts du dossier `.pio`, où sont + rangées les bibliothèques de `lib_deps` (voir chapitre Arduino 2). +- `check_src_filters` limite l'analyse à `src/main.cpp` et à + `lib/temp_logic/`. + +### Lancer l'analyse + +Depuis le dossier du projet : + +``` +$ pio check -e xiao --skip-packages --fail-on-defect=low +Checking xiao > cppcheck (platform: https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip; board: seeed_xiao_esp32s3; framework: arduino) +-------------------------------------------------------------------------------- +No defects found +========================== [PASSED] Took 0.52 seconds ========================== + +Environment Tool Status Duration +------------- -------- -------- ------------ +xiao cppcheck PASSED 00:00:00.524 +========================= 1 succeeded in 00:00:00.524 ========================= +``` + +- `-e xiao` : cppcheck voit les mêmes définitions que le compilateur + de la carte (`ARDUINO_XIAO_ESP32S3`, `ESP32`...). +- `--skip-packages` exclut les paquets de PlatformIO (framework + Arduino, chaîne de compilation) : leurs défauts ne vous concernent + pas, et l'analyse est bien plus rapide. +- `--fail-on-defect=low` fait échouer la commande (code de retour non + nul) dès le premier défaut, quelle que soit sa gravité. + +### Lire un défaut + +PlatformIO classe chaque défaut selon trois gravités : `high` pour une +erreur de cppcheck, `medium` pour un avertissement, `low` pour le +style et la performance. Pour voir un défaut, ajoutons une variable +inutile dans `toTenths`, dans `lib/temp_logic/temp_logic.cpp` : + +```cpp +int toTenths(float celsius) { + int arrondi = 0; + return (int)lroundf(celsius * 10.0f); +} +``` + +L'analyse échoue : + +``` +$ pio check -e xiao --skip-packages --fail-on-defect=low +Checking xiao > cppcheck (platform: ...; board: seeed_xiao_esp32s3; framework: arduino) +-------------------------------------------------------------------------------- +lib/temp_logic/temp_logic.cpp:27: [low:style] Variable 'arrondi' is assigned a value that is never used. [unreadVariable] +========================== [FAILED] Took 0.28 seconds ========================== + +Component HIGH MEDIUM LOW +-------------- ------ -------- ----- +lib/temp_logic 0 0 1 +... +==================== 1 failed, 0 succeeded in 00:00:00.276 ==================== +``` + +Chaque défaut donne le fichier et la ligne, la gravité et la famille +(`[low:style]`), le message, puis l'**identifiant** du contrôle +(`unreadVariable`). Sans `--fail-on-defect`, le défaut serait affiché +mais la commande finirait par `PASSED`, avec un code de retour nul. + +### Supprimer un défaut, en le justifiant + +Parfois, cppcheck a raison en général mais tort dans un cas précis. Le +rappel MQTT de `src/main.cpp` en est un exemple +([main.cpp](https://github.com/pguinet/arduino/blob/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT/src/main.cpp)) : + +```cpp +// Signature imposee par PubSubClient (pas de const possible). +// cppcheck-suppress constParameterCallback +static void onMqttMessage(char *topic, byte *payload, unsigned int length) { +``` + +La fonction ne modifie ni `topic` ni `payload` : cppcheck propose de +les déclarer `const`. Mais c'est un **rappel** (*callback*) : la +bibliothèque PubSubClient l'appelle et impose cette signature exacte. +Le commentaire `// cppcheck-suppress `, juste avant la +ligne, fait taire ce seul contrôle sur cette seule ligne (il exige +`--inline-suppr`). Supprimez toujours un identifiant précis, et +écrivez au-dessus **pourquoi** le défaut est accepté : un relecteur +doit pouvoir en juger sans enquêter. + +## L'intégration continue + +Sur votre PC, vous pouvez oublier les tests avant un commit, ou +dépendre d'une bibliothèque que vous seul avez installée. L'**intégration +continue** (*CI*) fait vérifier chaque modification, automatiquement, +sur une machine neuve : à chaque push, un serveur récupère le dépôt, +installe les outils, teste, compile et analyse. Le résultat s'affiche à +côté du commit et de la pull request (voir module additionnel Git). + +**GitHub Actions** est le service de CI intégré à GitHub. Son +vocabulaire : + +| Terme | Sens | +|-------|------| +| Workflow | Un fichier YAML dans `.github/workflows/` : quand se déclencher, et quoi faire | +| Run | Une exécution d'un workflow, déclenchée par un événement (push, pull request) | +| Job | Un groupe d'étapes exécutées sur une même machine ; les jobs tournent en parallèle sauf dépendance | +| Step | Une étape d'un job : une commande shell (`run:`) ou une action (`uses:`) | +| Runner | La machine qui exécute un job ; `ubuntu-latest` est une VM Ubuntu neuve, fournie par GitHub | +| Action | Une étape réutilisable publiée sur GitHub, par exemple `actions/checkout` qui récupère le dépôt | + +## Le fichier ci.yml décortiqué + +Le dépôt `arduino` contient plusieurs projets ; un seul workflow les +vérifie tous +([ci.yml](https://github.com/pguinet/arduino/blob/1af068f/.github/workflows/ci.yml)). +Voici ses 66 premières lignes, telles qu'au commit `1af068f` : + +```yaml +# Tests et qualite : projets PlatformIO avec tests natifs + agent Python +# de Claude_Dashboard. Le firmware est compile avec un credentials.h tire de +# credentials.h.example (le vrai n'est jamais dans le depot). + +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + discover: + name: Projets PlatformIO testes + runs-on: ubuntu-latest + outputs: + projects: ${{ steps.find.outputs.projects }} + steps: + - uses: actions/checkout@v7 + # Tout projet qui declare un [env:native] a des tests natifs. + - id: find + run: | + projects=$(grep -rl --include=platformio.ini '^\[env:native\]' sketches \ + | xargs -n1 dirname | sort | jq -R . | jq -cs .) + echo "projects=$projects" >> "$GITHUB_OUTPUT" + echo "Projets : $projects" + + platformio: + name: ${{ matrix.project }} + needs: discover + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: ${{ fromJSON(needs.discover.outputs.projects) }} + defaults: + run: + working-directory: ${{ matrix.project }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - uses: actions/cache@v6 + with: + path: ~/.platformio + key: pio-${{ runner.os }}-${{ hashFiles(format('{0}/platformio.ini', matrix.project)) }} + - name: Installer PlatformIO + run: pip install platformio==6.1.19 + - name: credentials.h factice + run: cp "$GITHUB_WORKSPACE/sketches/common/credentials.h.example" src/credentials.h + - name: Tests natifs + run: pio test -e native + - name: Environnement de la carte + id: env + run: | + env=$(python -c "import configparser; c = configparser.ConfigParser(); \ + c.read('platformio.ini'); print(c['platformio']['default_envs'].split(',')[0].strip())") + echo "name=$env" >> "$GITHUB_OUTPUT" + - name: Compilation du firmware + run: pio run -e ${{ steps.env.outputs.name }} + - name: cppcheck + run: pio check -e ${{ steps.env.outputs.name }} --skip-packages --fail-on-defect=low +``` + +Le fichier se termine par un troisième job, `claude-dash-agent`, qui +vérifie le serveur Python d'un autre projet du dépôt ; il ne concerne +pas ce module. + +### Déclencheurs et permissions + +- `on:` liste les événements qui lancent le workflow : un push sur la + branche `main`, et toute pull request, quelle que soit la branche + visée. Un push sur une autre branche ne déclenche rien tant qu'aucune + pull request n'est ouverte pour elle. +- `permissions: contents: read` limite le jeton confié au workflow à + la lecture du dépôt (moindre privilège : une étape compromise ne + pourrait rien pousser). + +### Le job discover : trouver les projets à vérifier + +Le job `discover` calcule la liste des projets : tout projet qui +déclare un `[env:native]` a des tests natifs. + +1. `grep -rl --include=platformio.ini '^\[env:native\]' sketches` liste + les `platformio.ini` qui contiennent cette ligne ; +2. `xargs -n1 dirname` garde le dossier de chacun, `sort` les trie ; +3. `jq -R .` transforme chaque ligne en chaîne JSON, `jq -cs .` les + rassemble en un tableau JSON compact ; +4. `echo "projects=..." >> "$GITHUB_OUTPUT"` publie le résultat comme + **sortie** du step `find` ; la clé `outputs:` du job la remonte au + niveau du job. + +Le log affiche +`Projets : ["sketches/JC3248W535C/Claude_Dashboard","sketches/XIAO-ESP32-S3/DS18B20_MQTT"]`. +Un nouveau projet testé est donc vérifié sans toucher au workflow. + +### Le job platformio : une matrice de projets + +- `needs: discover` attend la fin de `discover` et donne accès à ses + sorties (`needs.discover.outputs.projects`). +- `strategy.matrix` crée **une copie du job par valeur** de la liste. + `fromJSON(...)` convertit la chaîne JSON en vraie liste : la matrice + est dynamique. Avec deux projets, on obtient deux jobs en parallèle, + nommés d'après le projet grâce à `name: ${{ matrix.project }}`. +- `fail-fast: false` laisse tourner les autres jobs de la matrice quand + l'un échoue (par défaut, GitHub les annulerait). +- `defaults.run.working-directory` fait exécuter chaque `run:` dans le + dossier du projet : `pio` y trouve son `platformio.ini`. + +### Les steps + +1. `actions/checkout@v7` récupère le dépôt (`@v7` : version de + l'action) ; `actions/setup-python@v7` installe Python 3.12. +2. `actions/cache@v6` restaure `~/.platformio` (plateformes, chaînes + de compilation, bibliothèques : environ 1,4 Go pour ce projet) au + début du job ; sans cache pour la clé, il le sauvegarde à la fin. La + **clé** inclut une empreinte du `platformio.ini` (`hashFiles`) : + changer une version dans `lib_deps` repart d'un cache vide. +3. `pip install platformio==6.1.19` fige la version : sans `==`, une + nouvelle version publiée un matin pourrait casser la CI sans + qu'aucun commit ne l'explique. +4. `credentials.h factice` copie le modèle aux valeurs fictives, pour + que `main.cpp` compile : le vrai fichier n'est jamais versionné (voir + chapitre Arduino 4). +5. `Tests natifs` lance `pio test -e native` (voir chapitre Arduino 5). +6. `Environnement de la carte` lit `default_envs` avec le module Python + `configparser` et garde le premier nom (`xiao` ici) : chaque projet + vise une carte différente, aucune n'est codée en dur. Le nom, publié + par le step `env`, est relu par `${{ steps.env.outputs.name }}`. +7. `Compilation du firmware` lance `pio run -e xiao`, car les tests + natifs ne compilent pas le code de la carte. +8. `cppcheck` lance l'analyse avec `--fail-on-defect=low` : aucun + défaut, même de style, n'est toléré. + +Le premier step qui échoue arrête le job ; les suivants sont sautés. + +## Lire un résultat dans l'onglet Actions + +L'onglet **Actions** du dépôt liste les runs (titre du commit ou de la +pull request, workflow `CI`, branche, événement, durée), avec une +pastille verte ou une croix rouge. Un run affiche ses jobs : +`Projets PlatformIO testes`, un job par projet de la matrice +(`sketches/XIAO-ESP32-S3/DS18B20_MQTT`...) et les deux `Agent Python`. +Un job affiche ses steps repliables, chacun avec son log : `Set up job`, +`Run actions/checkout@v7`, ..., `Tests natifs`, `Compilation du +firmware`, `cppcheck`, puis `Post Run ...` et `Complete job`. Sur une +pull request, les mêmes jobs figurent dans la liste des vérifications +(*checks*), avec un lien **Details** vers le log. + +Voici un échec réel, sur une pull request antérieure au commit +`1af068f` : le job `sketches/JC3248W535C/Claude_Dashboard` est rouge, +ses steps sont verts jusqu'à `Compilation du firmware`, et `cppcheck` +porte la croix. Fin du log (horodatages retirés) : + +``` +Run pio check -e esp32s3 --skip-packages --fail-on-defect=low +Checking esp32s3 > cppcheck (platform: ...; board: esp32-s3-devkitc-1; framework: arduino) +-------------------------------------------------------------------------------- +Tool Manager: Installing platformio/tool-cppcheck @ ~1.21100.0 +... +src/ui.cpp:243: [low:style] Condition 'n>shown' is always false [knownConditionTrueFalse] +========================== [FAILED] Took 3.12 seconds ========================== +... +##[error]Process completed with exit code 1. +``` + +La méthode est toujours la même : le job rouge, puis le step rouge, +puis la première ligne du log qui explique l'échec (ici, une condition +jamais vraie, `ui.cpp` ligne 243). On reproduit avec `pio check` sur +son PC, on corrige, on pousse à nouveau. + +## La chaîne complète + +Mis bout à bout, les chapitres du module donnent cette chaîne : + +``` +éditer le code + -> pio test -e native (les tests passent, voir chapitre Arduino 5) + -> pio check -e xiao ... (aucun défaut) + -> git commit, git push (sur une branche, voir module additionnel Git) + -> pull request (la CI rejoue tout sur une VM neuve) + -> CI verte, relecture + -> fusion dans main (la CI tourne une dernière fois sur main) + -> pio run -t upload (flashage, avec la carte, voir chapitre Arduino 2) +``` + +Les vérifications locales répondent en quelques secondes ; la CI ne +les remplace pas, elle garantit qu'elles ont été faites, sur une +machine que personne n'a bricolée. + +## L'essentiel + +| Commande / Notion | Usage | +|-------------------|-------| +| Analyse statique | Chercher des défauts en lisant le code, sans l'exécuter | +| `check_tool`, `check_flags`, `check_src_filters` | Choisir l'analyseur, ses options et les fichiers analysés (`platformio.ini`) | +| `pio check -e xiao --skip-packages` | Analyser le projet avec la configuration de la carte, sans les paquets | +| `--fail-on-defect=low` | Faire échouer la commande dès le premier défaut | +| `[low:style] ... [unreadVariable]` | Gravité, famille, message et identifiant d'un défaut | +| `// cppcheck-suppress ` | Taire un contrôle sur la ligne suivante (avec `--inline-suppr`), en justifiant | +| Intégration continue | Vérifier automatiquement chaque modification sur une machine neuve | +| Workflow, job, step | Fichier `.github/workflows/*.yml`, groupe d'étapes sur un runner, étape | +| Runner, action (`uses:`) | Machine qui exécute un job ; étape réutilisable publiée | +| `on:`, `permissions:` | Événements déclencheurs ; droits du jeton du workflow | +| `needs`, `outputs`, `$GITHUB_OUTPUT` | Enchaîner des jobs et leur passer des valeurs | +| `matrix`, `fromJSON`, `fail-fast: false` | Un job par projet, liste calculée, pas d'annulation en cascade | +| `actions/cache` et `hashFiles` | Réutiliser `~/.platformio` tant que `platformio.ini` ne change pas | +| `platformio==6.1.19` | Figer la version de l'outil pour des runs reproductibles | +| Onglet Actions | Runs, jobs, steps et logs ; lire le premier step rouge | + +## Pour aller plus loin + +**Un badge de statut.** Le README d'un dépôt peut afficher l'état de +la CI sous forme d'image, mise à jour à chaque run. L'adresse suit la +forme +`https://github.com/PROPRIETAIRE/DEPOT/actions/workflows/FICHIER/badge.svg`. +Pour le dépôt `arduino`, en Markdown : + +```markdown +![CI](https://github.com/pguinet/arduino/actions/workflows/ci.yml/badge.svg) +``` + +Par défaut, le badge reflète la branche par défaut du dépôt. Les +paramètres `?branch=main` ou `?event=push`, ajoutés à la fin de +l'adresse, le restreignent à une branche ou à un événement. + +**Exiger la CI avant une fusion.** Une CI rouge n'empêche pas, à elle +seule, de fusionner une pull request. Une **règle de protection de +branche** le peut : dans **Settings**, section **Branches**, cliquer +**Add classic branch protection rule**, saisir `main` comme *Branch +name pattern*, cocher **Require status checks to pass before +merging**, puis rechercher et sélectionner les vérifications exigées. +Leurs noms sont ceux des jobs, par exemple +`sketches/XIAO-ESP32-S3/DS18B20_MQTT`. L'option **Require branches to +be up to date before merging** impose en plus que la branche intègre +le dernier état de `main`. GitHub propose aussi les **rulesets**, plus +souples, qui remplacent progressivement ces règles classiques. Le dépôt +`arduino` n'en a pas : c'est un projet personnel. + +**Lancer un workflow en local avec act.** L'outil libre `act` +(https://github.com/nektos/act) exécute les jobs d'un workflow dans +des conteneurs Docker sur votre PC (voir module additionnel Docker) : +`act -l` liste les jobs, `act pull_request` simule une pull request. +Les images par défaut ne reproduisent pas exactement les runners de +GitHub : utile pour mettre au point un workflow, pas pour remplacer la +vraie CI. + +**Tests sur la cible.** `pio test -e native` teste la logique pure sur +le PC. PlatformIO sait aussi compiler les tests pour la carte, les +flasher et lire leurs résultats sur le port série : +`pio test -e xiao`. Cela vérifie le code dans son vrai environnement +(processeur 32 bits, flottants de l'ESP32), mais demande la carte +branchée : impossible sur un runner GitHub, sauf à brancher la carte +sur un runner auto-hébergé. Le projet ne le fait pas. + +**Garder les actions à jour avec Dependabot.** Les versions des +actions (`@v7`, `@v6`) vieillissent : le commit `1af068f` les a +justement fait passer de v4 et v5 à v7 et v6, parce que GitHub +signalait leur version de Node.js comme obsolète. Dependabot peut +proposer ces mises à jour sous forme de pull requests, vérifiées par la +CI. Il suffit d'un fichier `.github/dependabot.yml` : + +```yaml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` + +**Comparer cppcheck d'une machine à l'autre.** La version de cppcheck +dépend du paquet installé par PlatformIO : le log ci-dessus montre +`tool-cppcheck @ ~1.21100.0` (cppcheck 2.11) sur le runner. Une version +plus récente sur votre PC peut trouver des défauts que la CI ne voit +pas, ou l'inverse. En cas de désaccord, la CI fait foi. + +## Exercices + +Les exercices 1 à 4 se font sans carte, dans une copie du dépôt +`arduino` avec un `credentials.h` copié depuis le modèle (voir chapitre +Arduino 4). Annulez vos modifications après chaque exercice +(`git restore .`). + +**Exercice 1.** Introduisez deux défauts dans +`lib/temp_logic/temp_logic.cpp`, puis faites-les signaler par +`pio check -e xiao --skip-packages --fail-on-defect=low` : + +1. une variable inutile, `int arrondi = 0;`, au début de `toTenths` ; +2. à la fin du fichier, une fonction qui reçoit une chaîne par valeur : + +```cpp +#include +namespace temp { +size_t labelLength(std::string label) { return label.size(); } +} +``` + +Pour chaque défaut, relevez la gravité, la famille et l'identifiant, +puis corrigez-le et vérifiez que l'analyse repasse. + +**Exercice 2.** Relancez l'analyse du premier défaut de l'exercice 1 +sans l'option `--fail-on-defect`. Que change le résultat ? Pourquoi +la CI utilise-t-elle cette option ? + +**Exercice 3.** Supprimez la ligne `// cppcheck-suppress +constParameterCallback` de `src/main.cpp`. Prévoyez le résultat de +`pio check`, puis vérifiez. Pourquoi ne corrige-t-on pas plutôt le +code en ajoutant `const` ? + +**Exercice 4.** Dans `sketches/common/credentials.h.example`, la +ligne `#define MQTT_SERVER ...` disparaît par erreur. Pour le job +`sketches/XIAO-ESP32-S3/DS18B20_MQTT`, quel est le premier step qui +échoue, et quel message lira-t-on dans son log ? Les tests natifs +passent-ils ? + +**Exercice 5.** *(Compte GitHub requis, sans carte.)* Faites un fork +du dépôt `pguinet/arduino` (voir module additionnel Git) et activez +GitHub Actions dans son onglet **Actions**. Créez une branche qui +casse un test, par exemple en remplaçant `85.0f` par `86.0f` dans la +constante `kPowerOn` de `temp_logic.cpp`, et poussez-la. La CI se +lance-t-elle ? Faites ce qu'il faut pour qu'elle se lance, puis +retrouvez dans le log le test en échec. + +**Exercice 6.** Votre équipe ajoute une branche `develop` : les +fonctionnalités y sont fusionnées par pull request, puis `develop` est +fusionnée dans `main` à chaque version. On veut que la CI tourne pour +les pull requests qui visent `develop` et pour chaque push sur +`develop`. Que faut-il modifier dans `ci.yml` ? + +### Solutions + +**Solution 1.** + +1. `lib/temp_logic/temp_logic.cpp:27: [low:style] Variable 'arrondi' + is assigned a value that is never used. [unreadVariable]` : gravité + `low`, famille `style`, identifiant `unreadVariable`. Correction : + supprimer la ligne. +2. `[low:performance] Function parameter 'label' should be passed by + const reference. [passedByValue]` : gravité `low`, famille + `performance`, identifiant `passedByValue`. Passer une + `std::string` par valeur la copie à chaque appel. Correction : + `size_t labelLength(const std::string &label)`. + +Après chaque correction, l'analyse affiche `No defects found` et +`PASSED`. + +**Solution 2.** Le défaut est toujours affiché, mais l'analyse se +termine par `PASSED` et le code de retour vaut 0. Un step GitHub +Actions n'échoue que si sa commande renvoie un code non nul : sans +l'option, la CI resterait verte malgré le défaut, et personne ne le +lirait. + +**Solution 3.** Deux défauts, un par paramètre : + +``` +src/main.cpp:71: [low:style] Parameter 'topic' can be declared as pointer to const. However it seems that 'onMqttMessage' is a callback function, if 'topic' is declared with const you might also need to cast function pointer(s). [constParameterCallback] +src/main.cpp:71: [low:style] Parameter 'payload' can be declared as pointer to const. However it seems that 'onMqttMessage' is a callback function, if 'payload' is declared with const you might also need to cast function pointer(s). [constParameterCallback] +``` + +cppcheck devine lui-même qu'il s'agit d'un rappel. La signature est +imposée par PubSubClient, qui appelle la fonction : avec `const`, son +type ne correspondrait plus à celui qu'attend `mqtt.setCallback`, et +la compilation échouerait. La suppression en ligne, justifiée par le +commentaire qui la précède, est la bonne réponse. + +**Solution 4.** Le step `credentials.h factice` réussit : il copie le +fichier, quel que soit son contenu. Les `Tests natifs` passent aussi : +ils compilent `lib/temp_logic` et les tests, pas `src/main.cpp` (voir +chapitre Arduino 5). Le premier step en échec est `Compilation du +firmware` : `main.cpp` contient une garde + +```cpp +#ifndef MQTT_SERVER +#error "credentials.h : MQTT_SERVER manquant (voir sketches/common/credentials.h.example)" +#endif +``` + +et le log du step affiche ce message d'erreur. Le step `cppcheck` est +sauté. + +**Solution 5.** La CI ne se lance pas : `on.push` ne vise que `main`, +et aucune pull request n'existe encore. Il faut ouvrir une pull +request de la branche vers `main` dans le fork (attention à choisir le +fork, et non `pguinet/arduino`, comme dépôt de base). Le run démarre ; +le job `sketches/XIAO-ESP32-S3/DS18B20_MQTT` échoue au step `Tests +natifs`, dont le log montre deux tests en échec dans +`test_reading` (voir chapitre Arduino 5) : + +``` +test/test_reading/test_main.cpp:16: test_power_on_85_rejected_without_close_previous: Expected FALSE Was TRUE [FAILED] +test/test_reading/test_main.cpp:23: test_real_85_accepted_when_previous_is_close: Expected FALSE Was TRUE [FAILED] +``` + +Avec `kPowerOn` à 86, la valeur 85 n'est plus reconnue comme la valeur +de mise sous tension : elle est acceptée alors que les tests attendent +un rejet. Les steps suivants sont sautés ; l'autre projet de +la matrice continue grâce à `fail-fast: false`. + +**Solution 6.** Pour les pull requests, rien : `pull_request:` sans +filtre couvre déjà toutes les branches visées, `develop` comprise. +Pour les push, il faut ajouter `develop` à la liste : + +```yaml +on: + push: + branches: [main, develop] + pull_request: +``` + +Si l'on voulait au contraire limiter les pull requests à ces deux +branches, on écrirait `pull_request:` suivi de +`branches: [main, develop]`, avec la même syntaxe que pour `push`. From 21a166a0c786fc4c97567bf7cb5f949794b1e066 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:14:09 +0200 Subject: [PATCH 10/12] docs: module additionnel Arduino dans le plan et le README Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 23 +++++++++++++++++++++-- README.md | 7 +++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0b604b4..50ed621 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,6 +108,18 @@ Les modules additionnels sont des modules complémentaires qui peuvent être sui *Prérequis : Modules 1-4 (navigation et manipulation de fichiers)* *Durée : 12-15 heures (module plus avancé)* +**Module additionnel Arduino : du sketch à l'intégration continue** (lecture personnelle, hors séances) +- Chapitre Arduino 1 : De l'Arduino à la XIAO ESP32-S3 +- Chapitre Arduino 2 : De l'IDE Arduino à PlatformIO +- Chapitre Arduino 3 : MQTT et Home Assistant +- Chapitre Arduino 4 : Lecture du code +- Chapitre Arduino 5 : Les tests unitaires +- Chapitre Arduino 6 : Qualité et intégration continue + +*Prérequis : avoir utilisé l'IDE Arduino ; module additionnel Git* +*Durée : 6-8 heures de lecture* +*Le code étudié n'est pas copié ici : il est cité depuis le dépôt public pguinet/arduino (sketches/XIAO-ESP32-S3/DS18B20_MQTT), au commit figé 1af068f.* + ## Structure des fichiers du projet ``` @@ -148,7 +160,7 @@ formation_linux/ installation_ssh.md (public A : se connecter à sa VM distante) installation_virtualbox.md (public B : Debian 13 dans VirtualBox, illustré) supports/ - modules_additionnels/ (toujours actif : modules Git et Docker) + modules_additionnels/ (toujours actif : modules Git, Git sous Windows, Docker, Arduino) module_git/ 01_introduction_git.md 02_commandes_base.md @@ -164,6 +176,13 @@ formation_linux/ 02_images_conteneurs.md 03_volumes_reseaux.md 04_compose_orchestration.md + module_arduino/ (lecture personnelle : PlatformIO, tests, CI) + 01_arduino_xiao_esp32s3.md + 02_ide_vers_platformio.md + 03_mqtt_home_assistant.md + 04_lecture_code.md + 05_tests_unitaires.md + 06_qualite_integration_continue.md travaux_pratiques/ tp_additionnels/ (toujours actif : TP Git et Docker) tp_git/ @@ -204,7 +223,7 @@ formation_linux/ plans/ (plans d'implémentation) ``` -**Remarque** : `archives/cours_2025/` contient l'ancien matériau du cours de base issu de la version précédente. Ne plus le modifier et ne plus le générer en PDF. Tout nouveau contenu de cours va dans `cours_2026/`. `supports/` et `travaux_pratiques/` ne contiennent plus que les modules additionnels (Git, Git sous Windows, Docker), toujours actifs. +**Remarque** : `archives/cours_2025/` contient l'ancien matériau du cours de base issu de la version précédente. Ne plus le modifier et ne plus le générer en PDF. Tout nouveau contenu de cours va dans `cours_2026/`. `supports/` et `travaux_pratiques/` ne contiennent plus que les modules additionnels (Git, Git sous Windows, Docker, Arduino), toujours actifs. ## Template de chapitre diff --git a/README.md b/README.md index 153253e..01c124e 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,12 @@ Introduction à Docker : images et conteneurs, volumes et réseaux, Docker Compo Contenu : `supports/modules_additionnels/module_docker/` TP : `travaux_pratiques/tp_additionnels/tp_docker/` +### Module Arduino -- Du sketch à l'intégration continue (6-8h de lecture) + +À lire chez soi, hors séances, pour qui connaît Arduino par l'IDE officiel et les fichiers `.ino` : la carte XIAO ESP32-S3 et la sonde DS18B20, PlatformIO, MQTT et Home Assistant, lecture du code d'un projet réel ([pguinet/arduino](https://github.com/pguinet/arduino)), tests unitaires natifs, cppcheck et GitHub Actions. Presque tout se fait sans carte. + +Contenu : `supports/modules_additionnels/module_arduino/` + ## Structure du dépôt @@ -131,6 +137,7 @@ formation_linux/ | +-- module_git/ | +-- module_git_windows/ | +-- module_docker/ +| +-- module_arduino/ | +-- travaux_pratiques/ | +-- tp_additionnels/ # Modules additionnels (TP) From 238f1cab177384afeaabe785c044ca8fd597fb36 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:22:17 +0200 Subject: [PATCH 11/12] docs: corrections de la relecture du module Arduino Co-Authored-By: Claude Opus 5.5 --- .../module_arduino/01_arduino_xiao_esp32s3.md | 83 ++++++++++--------- .../module_arduino/02_ide_vers_platformio.md | 61 +++++++++----- .../module_arduino/03_mqtt_home_assistant.md | 33 +++++--- .../module_arduino/04_lecture_code.md | 63 ++++++++++---- .../module_arduino/05_tests_unitaires.md | 69 ++++++++------- .../06_qualite_integration_continue.md | 40 +++++---- 6 files changed, 211 insertions(+), 138 deletions(-) diff --git a/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md index 9513376..8acc42f 100644 --- a/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md +++ b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md @@ -7,7 +7,7 @@ > - décrire la sonde DS18B20, le bus 1-Wire et le rôle de la résistance de tirage ; > - vous repérer dans l'arborescence du projet étudié dans ce module. > -> **Durée de lecture** : environ 45 min. +> **Durée de lecture** : environ 45 min, exercices compris. --- @@ -31,10 +31,11 @@ DS18B20 --1-Wire--> XIAO ESP32-S3 --WiFi--> broker MQTT --> Home Assistant (sonde) (la carte) (serveur) (affichage) ``` -La carte lit la sonde toutes les 10 secondes et envoie la mesure par -WiFi à un **broker MQTT**, un serveur qui reçoit les messages et les -redistribue ; Home Assistant les reçoit, affiche la mesure et en garde -l'historique. +La carte lit la sonde toutes les 10 secondes. Elle publie la mesure par +WiFi quand elle a changé d'au moins 0,2 °C, et au plus tard toutes les +5 minutes (voir chapitre Arduino 4). Les messages partent vers un +**broker MQTT**, un serveur qui les reçoit et les redistribue ; Home +Assistant les reçoit, affiche la mesure et en garde l'historique. Le code se trouve dans le dépôt https://github.com/pguinet/arduino, dossier `sketches/XIAO-ESP32-S3/DS18B20_MQTT`. Le module cite toujours @@ -45,8 +46,8 @@ https://github.com/pguinet/arduino/tree/1af068f/sketches/XIAO-ESP32-S3/DS18B20_M ### Prérequis et organisation - avoir déjà écrit et téléversé un sketch avec l'IDE Arduino ; -- avoir lu le module additionnel Git : cloner, commit, branche, GitHub - ne sont pas réexpliqués (voir module additionnel Git). +- avoir lu le module additionnel Git (contrôle de version) : cloner, + commit, branche, GitHub ne sont pas réexpliqués. Le module se lit seul, sur votre propre PC, en 6 à 8 heures. Linux sert d'exemple ; les différences avec Windows et macOS sont signalées. @@ -61,14 +62,14 @@ chaque exercice indique s'il la demande : | lancer l'analyse statique (cppcheck) | voir les messages MQTT réels | | faire tourner l'intégration continue | voir le capteur dans Home Assistant | -| Chapitre | Contenu | -|----------|---------| -| Arduino 1 | Le matériel : ESP32-S3, XIAO, DS18B20, bus 1-Wire, le projet | -| Arduino 2 | De l'IDE Arduino à PlatformIO : `platformio.ini`, `pio run` | -| Arduino 3 | MQTT et Home Assistant : sujets, messages retenus, découverte | -| Arduino 4 | Lecture du code : architecture, boucle non bloquante, reconnexions | -| Arduino 5 | Les tests unitaires : Unity, `pio test`, écrire un test | -| Arduino 6 | Qualité et intégration continue : cppcheck, GitHub Actions | +| Chapitre | Titre | Contenu | +|----------|-------|---------| +| Arduino 1 | De l'Arduino à la XIAO ESP32-S3 | ESP32-S3, XIAO, DS18B20, bus 1-Wire, le projet | +| Arduino 2 | De l'IDE Arduino à PlatformIO | `platformio.ini`, `pio run` | +| Arduino 3 | MQTT et Home Assistant | sujets, messages retenus, découverte | +| Arduino 4 | Lecture du code | architecture, boucle non bloquante, reconnexions | +| Arduino 5 | Les tests unitaires | Unity, `pio test`, écrire un test | +| Arduino 6 | Qualité et intégration continue | cppcheck, GitHub Actions | ## Arduino, c'est trois choses @@ -135,7 +136,7 @@ elle, la carte ne voit pas le réseau, même à un mètre de la box. Le programme du projet l'affiche alors sur le moniteur série : ``` -WiFi toujours absent (statut 1) +WiFi toujours absent (statut 1) : relance ``` Le statut 1 est `WL_NO_SSID_AVAIL` : aucun réseau de ce nom n'est @@ -200,7 +201,7 @@ bus. C'est cette ligne de `setup()`, dans `src/main.cpp` : pinMode(ONE_WIRE_PIN, INPUT_PULLUP); ``` -Cette résistance, dix fois plus forte, suffit pour le mètre de câble +Cette résistance, dix fois plus élevée, donc un tirage plus faible, suffit pour le mètre de câble d'une sonde étanche, pas pour plusieurs mètres. En cas de lectures à -127, on soude une 4,7 kilohms entre `DATA` et `3V3`, sans toucher au code. Le câblage, repris du README : @@ -268,13 +269,13 @@ identifiants du WiFi et du broker : il est volontairement exclu du dépôt | Core | Implémentation du framework pour une famille de puces (`arduino-esp32` pour les ESP32) | | ESP32-S3 | Microcontrôleur Espressif : 2 cœurs à 240 MHz, WiFi, Bluetooth 5 LE, USB natif | | XIAO ESP32-S3 | Carte Seeed Studio de 21 x 17,8 mm, 8 Mo de flash, broches `D0` à `D10` | -| Antenne U.FL | Obligatoire sur la XIAO ; sans elle : `WiFi toujours absent (statut 1)` | +| Antenne U.FL | Obligatoire sur la XIAO ; sans elle : `WiFi toujours absent (statut 1) : relance` | | USB natif | La puce gère l'USB elle-même ; port `/dev/ttyACM0` (Linux), `COMx` (Windows) | | `303a:1001` | Identifiant USB commun à toutes les ESP32-S3 (`lsusb`) | | DS18B20 | Sonde numérique, -55 à +125 °C, 750 ms par conversion en 12 bits | | 1-Wire | Bus à un fil de données ; chaque sonde a une adresse unique de 64 bits | | -127 et 85 | Sonde muette, et valeur de mise sous tension : lectures suspectes | -| Pull-up | Ramène `DATA` à 3,3 V au repos ; interne (~45 kilohms) ou externe (4,7 kilohms) | +| Pull-up | Ramène `DATA` à 3,3 V au repos ; interne (environ 45 kilohms) ou externe (4,7 kilohms) | | `INPUT_PULLUP` | Active la résistance de tirage interne d'une broche | ## Pour aller plus loin @@ -288,14 +289,11 @@ reconnaissance d'images) et son grand nombre de broches. Le boîtier du dossier `case/` est prévu pour les XIAO C3, C6 et S3, qui ont le même format. -**ESP-IDF et FreeRTOS.** Sur l'ESP32, le framework -Arduino tourne au-dessus d'ESP-IDF, le kit de développement officiel -d'Espressif, et de FreeRTOS, un petit système temps réel. `loop()` est -en fait une tâche FreeRTOS, et le WiFi tourne dans d'autres tâches. On -peut appeler directement les fonctions d'ESP-IDF depuis un sketch : le -projet le fait avec `esp_read_mac()`, qui lit l'adresse MAC dans les -**eFuses** de la puce, une mémoire programmée une seule fois en usine -(voir chapitre Arduino 4). +**ESP-IDF.** Sur l'ESP32, le framework Arduino tourne au-dessus +d'ESP-IDF, le kit de développement officiel d'Espressif, lui-même bâti +sur FreeRTOS (voir chapitre Arduino 4). On peut appeler directement les +fonctions d'ESP-IDF depuis un sketch : le projet le fait avec +`esp_read_mac()` (voir chapitre Arduino 4). **Quand l'USB natif disparaît.** Avec une seconde puce USB, comme sur l'Uno, le port série existe tant que la carte est alimentée. Avec l'USB @@ -311,9 +309,8 @@ de la carte. Un appareil qui se présente comme un modem USB standard (classe CDC ACM), comme l'ESP32-S3 ou l'ATmega16U2 de l'Uno R3, devient `/dev/ttyACM0`. Les cartes équipées d'une puce de conversion USB-série (CH340, CP2102, FT232), fréquentes sur les clones et les anciennes -cartes ESP32, deviennent `/dev/ttyUSB0`. Pour accéder à ces fichiers sans -`sudo`, l'utilisateur doit appartenir au groupe `dialout` (voir -chapitre Arduino 2). +cartes ESP32, deviennent `/dev/ttyUSB0`. L'accès à ces fichiers sans +`sudo` demande un réglage (voir chapitre Arduino 2). **Le mode parasite.** La DS18B20 peut aussi fonctionner avec deux fils seulement (`DATA` et la masse) : elle se nourrit alors sur la ligne de @@ -329,22 +326,22 @@ bien des suppositions. ## Exercices -**Exercice 1** (avec la carte). Branchez la XIAO ESP32-S3 sur votre PC +**Exercice 1 -- Trouver la carte** (avec la carte). Branchez la XIAO ESP32-S3 sur votre PC Linux. Quelles commandes permettent de trouver le nom du port série et de vérifier que le PC voit une puce ESP32-S3 ? Comment distinguer la XIAO d'une autre carte ESP32-S3 ? -**Exercice 2** (sans carte). Un camarade a câblé une sonde DS18B20 sur +**Exercice 2 -- Sans résistance de tirage** (sans carte). Un camarade a câblé une sonde DS18B20 sur une XIAO comme dans le tableau de câblage, mais il a supprimé la ligne `pinMode(ONE_WIRE_PIN, INPUT_PULLUP);` de `setup()`. Il n'a pas soudé de résistance externe. Que va-t-il observer, et pourquoi ? Proposez deux corrections. -**Exercice 3** (sans carte). Votre XIAO affiche en boucle -`WiFi toujours absent (statut 1)`, alors que le nom du réseau est +**Exercice 3 -- Pas de WiFi** (sans carte). Votre XIAO affiche en boucle +`WiFi toujours absent (statut 1) : relance`, alors que le nom du réseau est correct. Quelle est la cause la plus probable ? -**Exercice 4** (sans carte). Pour chaque besoin, indiquez le fichier ou +**Exercice 4 -- Le bon fichier** (sans carte). Pour chaque besoin, indiquez le fichier ou le dossier du projet concerné : 1. changer la broche à laquelle la sonde est reliée ; @@ -353,13 +350,13 @@ le dossier du projet concerné : 4. imprimer un boîtier ; 5. renseigner le mot de passe du WiFi. -**Exercice 5** (sans carte). Clonez le dépôt du projet, placez-vous au +**Exercice 5 -- Cloner le projet** (sans carte). Clonez le dépôt du projet, placez-vous au commit `1af068f` utilisé dans tout le module, et vérifiez que le dossier du projet contient bien les fichiers listés dans ce chapitre. ### Solutions -**Solution 1.** +**Solution 1** ```bash ls /dev/ttyACM* @@ -375,7 +372,9 @@ distinguer la XIAO d'une autre carte. Le README du projet lit pour cela la taille de la mémoire flash avec `esptool` (`flash_id`) : 8 Mo pour la XIAO ESP32-S3. -**Solution 2.** Sans résistance de tirage, la ligne `DATA` n'est +**Solution 2** + +Sans résistance de tirage, la ligne `DATA` n'est ramenée à 3,3 V par rien : elle flotte, et les bits envoyés par la sonde sont illisibles. La sonde peut ne pas être détectée (`Sondes detectees : 0`), ou les lectures valent -127 et sont rejetées. @@ -384,12 +383,14 @@ avant `sensors.begin()`, ou souder une résistance de 4,7 kilohms entre `DATA` et `3V3`. La seconde solution est préférable avec un câble de plusieurs mètres. -**Solution 3.** L'antenne n'est pas branchée sur le connecteur U.FL. La +**Solution 3** + +L'antenne n'est pas branchée sur le connecteur U.FL. La XIAO ESP32-S3 n'a pas d'antenne intégrée : sans elle, aucun réseau n'est visible, d'où le statut 1 (`WL_NO_SSID_AVAIL`). Autre cause possible : un réseau hors de portée. -**Solution 4.** +**Solution 4** 1. `src/main.cpp` (la constante `ONE_WIRE_PIN`). 2. `platformio.ini` (voir chapitre Arduino 2). @@ -399,7 +400,7 @@ un réseau hors de portée. 5. `src/credentials.h`, absent du dépôt, à créer à partir du modèle (voir chapitre Arduino 4). -**Solution 5.** +**Solution 5** ```bash git clone https://github.com/pguinet/arduino.git diff --git a/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md index ff07be2..5ef0104 100644 --- a/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md +++ b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md @@ -7,7 +7,7 @@ > - compiler, téléverser et ouvrir le moniteur série avec `pio run` et `pio device monitor` ; > - distinguer les bibliothèques du registre (`lib_deps`) et les bibliothèques locales (`lib/`). > -> **Durée de lecture** : environ 1 h, exercices compris. Seuls le téléversement et le moniteur série demandent la carte. +> **Durée de lecture** : environ 1 h, exercices compris. --- @@ -207,9 +207,13 @@ le framework natif d'Espressif. **Le port et le moniteur.** `upload_port` et `monitor_port` fixent le port (sans eux, PlatformIO le cherche seul) : on ne flashe pas la mauvaise carte. Sous Windows, ce serait `COM3` par exemple ; sous macOS, -`/dev/cu.usbmodem...`. `monitor_speed` doit égaler le -`Serial.begin(115200)` du programme : sinon, le moniteur écoute à -9600 bauds et n'affiche que des caractères illisibles. +`/dev/cu.usbmodem...`. Par convention, `monitor_speed` égale le +`Serial.begin(115200)` du programme (le moniteur écoute sinon à 9600 +bauds). C'est indispensable sur les cartes dont l'USB passe par une +puce USB-série (Uno, clones à CH340) : une vitesse différente n'y +affiche que des caractères illisibles. Sur la XIAO, la carte définit +`ARDUINO_USB_CDC_ON_BOOT=1` : `Serial` passe par l'USB natif, qui +ignore la vitesse, et le réglage est sans effet. **La suite.** `lib_deps` liste les bibliothèques (voir plus bas). Les lignes `check_*` configurent cppcheck, lancé par `pio check` (voir @@ -255,7 +259,11 @@ Flash: [=== ] 26.7% (used 893329 bytes from 3342336 bytes) ``` Les bibliothèques sont téléchargées dans `.pio/libdeps/xiao/`, dans le -projet (`^7.0.0` résolu en `7.4.3`). Suivent la plateforme et la carte, +projet (`^7.0.0` résolu en `7.4.3`). Suivent la plateforme et la carte. +La sortie affiche 51.3.6 alors que l'URL désigne la release 51.03.07 : +le `platform.json` de cette release annonce cette version, ne vous en +inquiétez pas. Les 320KB de RAM sont la mémoire disponible pour le +programme ; le reste des 512 Ko de SRAM est réservé. Viennent ensuite les bibliothèques réellement utilisées, dont `temp_logic` (locale) et `WiFi` (fournie par le core), puis l'occupation mémoire, comme en bas de l'IDE. La compilation demande aussi `src/credentials.h`, absent du @@ -380,11 +388,9 @@ identifiant matériel. Les deux options `--upload-port` de `pio run` et `-p` de `pio device monitor` remplacent ponctuellement le port du fichier. -**Le téléversement de la XIAO ESP32-S3.** Si le programme en place -plante au démarrage, le port USB natif peut disparaître avant le -téléversement. On force alors le mode de téléversement : maintenir le -bouton `B` (BOOT), brancher la carte, relâcher, puis relancer -`pio run -e xiao -t upload`. +**Le téléversement de la XIAO ESP32-S3.** Si le port USB natif a +disparu, démarrez la carte bouton BOOT enfoncé, puis relancez +`pio run -e xiao -t upload` (voir chapitre Arduino 1). **Garder des `.ino` ?** PlatformIO sait compiler un `.ino` placé dans `src/` : il fait la même conversion que l'IDE (« Converting main.ino » @@ -398,7 +404,7 @@ chaque commande. ## Exercices -**Exercice 1 -- Porter un Blink (sans carte).** Créez un projet vide +**Exercice 1 -- Porter un Blink** (sans carte). Créez un projet vide dans un nouveau dossier : ```bash @@ -411,7 +417,7 @@ projet `DS18B20_MQTT`. Copiez le Blink du début du chapitre dans `src/main.cpp`, compilez avec `pio run`, puis corrigez le fichier jusqu'à ce que la compilation réussisse. -**Exercice 2 -- Ajouter une bibliothèque (sans carte).** Dans le projet +**Exercice 2 -- Ajouter une bibliothèque** (sans carte). Dans le projet de l'exercice 1, déclarez `bblanchon/ArduinoJson@^7.0.0` dans `lib_deps`. Écrivez un `main.cpp` qui construit un `JsonDocument` contenant une température et l'écrit sur `Serial` toutes les 2 @@ -419,24 +425,26 @@ secondes, puis compilez. Où la bibliothèque a-t-elle été installée ? Quelle ligne faut-il aussi ajouter pour lire ce message avec `pio device monitor` ? -**Exercice 3 -- Contraintes de version.** Pour chaque ligne de +**Exercice 3 -- Contraintes de version** (sans carte). Pour chaque ligne de `lib_deps` du projet, donnez la plus petite et la plus grande version acceptées. La version 3.0.0 de `OneWire` serait-elle installée ? Et `PubSubClient` 2.8.1 ? -**Exercice 4 -- Les environnements (sans carte).** Dans le projet +**Exercice 4 -- Les environnements** (sans carte). Dans le projet `DS18B20_MQTT`, quels sont les environnements ? Que compile `pio run` sans option ? Que se passe-t-il avec `pio run -e native`, et pourquoi cet environnement sert-il malgré tout ? -**Exercice 5 -- Reproductibilité.** Un collègue vous envoie un sketch +**Exercice 5 -- Reproductibilité** (sans carte). Un collègue vous envoie un sketch `.ino` qui « marche chez lui ». Listez les informations qui vous manquent pour le compiler à l'identique, et indiquez où chacune se trouverait dans un projet PlatformIO. ### Solutions -**Solution 1.** `pio project init` crée `platformio.ini`, `src/`, +**Solution 1** + +`pio project init` crée `platformio.ini`, `src/`, `lib/`, `include/` et `test/`, avec un environnement `[env:seeed_xiao_esp32s3]`. La ligne `platform` générée dépend de votre installation. Si la commande se termine par `IncompatiblePlatform` (voir @@ -466,7 +474,9 @@ Avec `#include ` seul, il reste une erreur : compilation se termine par `[SUCCESS]`. Autre solution : déplacer la fonction `clignoter()` avant `loop()`. -**Solution 2.** Le `platformio.ini` se termine par : +**Solution 2** + +Le `platformio.ini` se termine par : ```ini lib_deps = @@ -497,10 +507,13 @@ void loop() { `pio run` affiche `Library Manager: ArduinoJson@7.4.3 has been installed!` (ou une version 7.x plus récente) et la bibliothèque est rangée dans `.pio/libdeps/seeed_xiao_esp32s3/`, à l'intérieur du -projet. `monitor_speed = 115200` est nécessaire, car le moniteur écoute -à 9600 bauds par défaut ; la lecture elle-même demande la carte. +projet. Par convention, on ajoute `monitor_speed = 115200`, égal au +`Serial.begin(115200)` du programme. Sur la XIAO, dont `Serial` passe +par l'USB natif, il est sans effet ; il serait indispensable sur une +carte à puce USB-série (Uno, CH340). La lecture elle-même demande la +carte. -**Solution 3.** +**Solution 3** | Bibliothèque | Minimum | Maximum (exclu) | |--------------|---------|-----------------| @@ -512,7 +525,9 @@ projet. `monitor_speed = 115200` est nécessaire, car le moniteur écoute `OneWire` 3.0.0 est refusée : c'est une nouvelle version majeure. `PubSubClient` 2.8.1 est acceptée. -**Solution 4.** Deux environnements : `xiao` et `native`. `pio run` sans +**Solution 4** + +Deux environnements : `xiao` et `native`. `pio run` sans `-e` compile `default_envs`, donc `xiao` seulement. `pio run -e native` essaie de compiler `src/main.cpp` pour le PC et échoue : @@ -525,7 +540,9 @@ pas à compiler le programme, mais à exécuter les tests de `test/` avec `pio test -e native` : ils n'utilisent que `lib/temp_logic`, qui ne dépend pas d'Arduino (voir chapitre Arduino 5). -**Solution 5.** Il manque : +**Solution 5** + +Il manque : - la carte et ses options (`board` dans `[env:...]`) ; - la version du core, c'est-à-dire de la plateforme (`platform`, figée diff --git a/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md index 7cc8757..657c36b 100644 --- a/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md +++ b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md @@ -7,7 +7,7 @@ > - lire le message de découverte qui crée le capteur dans Home Assistant ; > - associer chaque sujet du projet à son contenu. > -> **Durée de lecture** : environ 1 h. +> **Durée de lecture** : environ 1 h, exercices compris. --- @@ -93,8 +93,11 @@ choisi à chaque publication : | 2 | exactement une fois | deux allers-retours | Le projet publie tout en QoS 0, la seule que `PubSubClient` sait -publier : perdre une mesure envoyée toutes les 10 secondes est sans -importance. +publier. La carte lit la sonde toutes les 10 secondes, mais ne publie +qu'à un écart d'au moins 0,2 °C, ou au plus tard toutes les 5 minutes +(voir chapitre Arduino 4). Une mesure perdue est donc rattrapée au +prochain changement, ou au plus tard par le battement de cœur des +5 minutes : la perte est sans gravité. **Session.** Ce que le broker garde pour un client : ses abonnements et, en QoS 1 ou 2, les messages en attente. La carte demande une session @@ -326,12 +329,12 @@ de chacun. Tous les exercices se font sans carte. L'exercice 5 demande Docker. -**Exercice 1.** La carte fonctionne depuis une heure. Un nouveau client +**Exercice 1 -- Les messages retenus** (sans carte). La carte fonctionne depuis une heure. Un nouveau client s'abonne à `xiao_ds18b20/#` et à `homeassistant/#`. Que reçoit-il immédiatement, avant toute nouvelle publication ? Et si la carte a été débranchée il y a dix minutes ? -**Exercice 2.** Plusieurs cartes publient sur le même broker. Écrivez : +**Exercice 2 -- Écrire des filtres** (sans carte). Plusieurs cartes publient sur le même broker. Écrivez : 1. le filtre qui reçoit la température de toutes les cartes, et rien d'autre ; @@ -340,19 +343,19 @@ débranchée il y a dix minutes ? 3. le filtre qui reçoit les messages de découverte de tous les capteurs annoncés à Home Assistant. -**Exercice 3.** Pourquoi la température n'est-elle pas publiée en message +**Exercice 3 -- Retenir ou non** (sans carte). Pourquoi la température n'est-elle pas publiée en message retenu, alors que `status` et `probe` le sont ? Décrivez ce que verrait Home Assistant, au redémarrage, si elle l'était et que la carte était hors service. -**Exercice 4.** Dans chaque situation, dites ce que valent `status` et +**Exercice 4 -- La disponibilité** (sans carte). Dans chaque situation, dites ce que valent `status` et `probe`, et ce qu'affiche Home Assistant : 1. tout fonctionne ; 2. le fil DATA de la sonde est coupé, la carte reste connectée ; 3. la carte n'est plus alimentée. -**Exercice 5 (Docker).** Simulez la carte avec un broker Mosquitto local +**Exercice 5 -- Un broker local** (sans carte, avec Docker). Simulez la carte avec un broker Mosquitto local et les outils `mosquitto_pub` et `mosquitto_sub` fournis dans l'image : 1. lancez le broker `eclipse-mosquitto:2` en autorisant les connexions @@ -366,7 +369,9 @@ et les outils `mosquitto_pub` et `mosquitto_sub` fournis dans l'image : ### Solutions -**Solution 1.** Il reçoit immédiatement les messages retenus : le JSON +**Solution 1** + +Il reçoit immédiatement les messages retenus : le JSON de découverte sur `homeassistant/sensor//temperature/config`, `online` sur `status` et `online` sur `probe` (si la sonde répond). Il ne reçoit pas la température, non retenue : il l'aura à la prochaine @@ -375,21 +380,23 @@ publié le testament : `status` vaut `offline`. La découverte et `probe` (`online`, dernière valeur publiée) restent inchangés ; c'est `availability_mode: all` qui rend malgré tout le capteur indisponible. -**Solution 2.** +**Solution 2** 1. `xiao_ds18b20/+/temperature` 2. `xiao_ds18b20/xiao_ds18b20_3bb004/#` 3. `homeassistant/+/+/+/config`, ou plus largement `homeassistant/#`, qui reçoit aussi `homeassistant/status`. -**Solution 3.** Une mesure n'a de sens qu'au moment où elle est prise. +**Solution 3** + +Une mesure n'a de sens qu'au moment où elle est prise. Retenue, la dernière valeur serait remise à chaque nouvel abonné, même longtemps après. Au redémarrage, Home Assistant lirait par exemple `21.4` et le présenterait comme la valeur courante. Ici, le testament fait passer `status` à `offline` et le capteur s'affiche « indisponible », ce qui est exact. -**Solution 4.** +**Solution 4** 1. `status` = `online`, `probe` = `online` : la température s'affiche. 2. `status` = `online`, `probe` = `offline` après trois lectures @@ -399,7 +406,7 @@ fait passer `status` à `offline` et le capteur s'affiche puisque `availability_mode: all` exige que les deux sujets valent `online`. -**Solution 5.** +**Solution 5** 1. Créez la configuration et lancez le broker : diff --git a/supports/modules_additionnels/module_arduino/04_lecture_code.md b/supports/modules_additionnels/module_arduino/04_lecture_code.md index 2e60cea..a29396e 100644 --- a/supports/modules_additionnels/module_arduino/04_lecture_code.md +++ b/supports/modules_additionnels/module_arduino/04_lecture_code.md @@ -8,7 +8,7 @@ > - expliquer la validation des lectures, `PublishPolicy` et `ProbeHealth` ; > - suivre la reconnexion WiFi et MQTT sans bloquer le programme. > -> **Durée de lecture** : ~1 h 30. +> **Durée de lecture** : environ 1 h 30, exercices compris. --- @@ -61,10 +61,21 @@ finir sur GitHub (`main.cpp`, lignes 19 à 28) : - **le fichier est ignoré** : le `.gitignore` du dépôt contient `**/credentials.h` (voir module additionnel Git) ; - **un modèle est versionné** : `sketches/common/credentials.h.example`, - avec des valeurs factices, à copier en `credentials.h` ; + avec des valeurs factices ; - **le compilateur vérifie** : `#error` arrête la compilation avec un message qui dit quoi faire. +Pour compiler, copiez le modèle dans `src/` depuis le dossier du projet +(`sketches/XIAO-ESP32-S3/DS18B20_MQTT`), deux niveaux sous `sketches/` : + +```bash +cp ../../common/credentials.h.example src/credentials.h +``` + +Puis éditez `src/credentials.h` (avec nano, par exemple) pour y mettre le +nom et le mot de passe de votre WiFi, l'adresse du broker et ses +identifiants. Les valeurs factices suffisent pour compiler et tester. + Voici `pio run -e xiao` quand `WIFI_SSID` manque : ``` @@ -98,10 +109,12 @@ Extraits de `setup()` (lignes 168 à 196) : - **`setWaitForConversion(false)`** : sinon `requestTemperatures()` attend la fin de la mesure (750 ms à 12 bits). On gère cette attente soi-même, sans bloquer. -- **`esp_read_mac()`** : l'identifiant (`xiao_ds18b20_1a2b3c`) dérive +- **`esp_read_mac()`** : l'identifiant (`xiao_ds18b20_3bb004`) dérive de la MAC. `WiFi.macAddress()` renverrait ici des zéros, la station n'étant pas démarrée : toutes les cartes auraient le même identifiant. - `esp_read_mac()` lit l'adresse gravée en usine (l'eFuse). + `esp_read_mac()`, fonction d'ESP-IDF, lit l'adresse gravée en usine + dans les **eFuses** de la puce, une mémoire programmable une seule + fois. - **`WiFi.begin()`** rend la main aussitôt, sans boucle `while (WiFi.status() != WL_CONNECTED)` : `loop()` suivra le réseau. - **`setBufferSize(1024)`** : PubSubClient limite un paquet à 256 @@ -241,8 +254,8 @@ bool PublishPolicy::shouldPublish(int tenths, uint32_t nowMs) const { Par défaut (2 dixièmes, 300 000 ms), on publie la première valeur, puis à chaque écart d'au moins 0,2 °C, et au moins toutes les 5 minutes (un -« battement de coeur »). `reset()`, appelé à chaque connexion MQTT, fait -partir la valeur suivante. Les **dixièmes entiers** (`toTenths(21.4375)` +« battement de cœur », *heartbeat* en anglais). `reset()`, appelé à +chaque connexion MQTT, fait partir la valeur suivante. Les **dixièmes entiers** (`toTenths(21.4375)` vaut 214) se comparent exactement, contrairement aux `float` (voir « Pour aller plus loin »). @@ -291,6 +304,8 @@ remise à l'heure à la perte du réseau laisse 30 s au core. if (millis() - lastAttempt < RETRY_INTERVAL_MS) return; const bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS, topics.status, 0, true, "offline"); lastAttempt = millis(); // delai compte depuis la fin de la tentative +... + mqtt.subscribe(HA_STATUS_TOPIC); ``` - **`0 - RETRY_INTERVAL_MS`** vaut 4 294 962 296. Au premier appel, @@ -303,6 +318,10 @@ remise à l'heure à la perte du réseau laisse 30 s au core. entre deux essais, même si le broker ne répond jamais. - **`mqtt.loop()`** lit les messages et entretient la connexion : il doit être appelé souvent, d'où l'interdiction de bloquer. +- **`mqtt.subscribe()` après chaque connexion** : la carte demande une + session propre (voir chapitre Arduino 3), et l'abonnement à + `homeassistant/status` est perdu avec la connexion. Il est donc + renouvelé à chaque connexion réussie. Les messages reçus arrivent dans le rappel `onMqttMessage()` (lignes 72 à 77) : @@ -360,7 +379,7 @@ piège ne se présente pas. `lastRead != 0` sert au démarrage. Si, après un débordement, `millis()` valait exactement 0 au moment d'une lecture, la lecture suivante partirait sans attendre 10 s. La conséquence est une mesure en avance, -une fois tous les 49 jours au pire : l'auteur a jugé ce cas acceptable. +une fois tous les 49 jours au pire : ce cas est sans gravité. `handleMqtt()` montre l'autre solution, sans cas particulier : initialiser à `0 - INTERVALLE`. @@ -387,7 +406,7 @@ MQTT asynchrone. Aucun exercice ne demande la carte. L'exercice 1 se vérifie avec `pio run` sur un projet PlatformIO vide (voir chapitre Arduino 2). -**Exercice 1.** Ce sketch fait clignoter la LED et affiche une mesure +**Exercice 1 -- Sans delay()** (sans carte). Ce sketch fait clignoter la LED et affiche une mesure analogique, mais la LED ne change d'état que toutes les 10 s : ```cpp @@ -410,12 +429,12 @@ Réécrivez-le sans `delay()` long : la LED change d'état toutes les démarrage. Suivez le style du projet (une fonction `handle*()` par tâche). -**Exercice 2.** Une lecture a eu lieu à `lastRead = 4 294 965 000`. +**Exercice 2 -- Le débordement** (sans carte). Une lecture a eu lieu à `lastRead = 4 294 965 000`. L'intervalle est de 10 000 ms. Calculez `millis() - lastRead` pour `millis() = 3 000`, puis `millis() = 9 000`. Dans chaque cas, la lecture suivante a-t-elle lieu ? -**Exercice 3.** Pour chaque appel, dites ce que renvoie +**Exercice 3 -- Valider une lecture** (sans carte). Pour chaque appel, dites ce que renvoie `isValidReading(celsius, hasPrevious, previous)` et pourquoi : 1. `(21.5, false, 0)` @@ -426,18 +445,20 @@ lecture suivante a-t-elle lieu ? 6. `(125.5, true, 20)` 7. `(84.9375, false, 0)` -**Exercice 4.** Une `PublishPolicy` par défaut reçoit, toutes les 10 s +**Exercice 4 -- Quand publier** (sans carte). Une `PublishPolicy` par défaut reçoit, toutes les 10 s à partir de `t = 0`, les valeurs en dixièmes 214, 215, 216, 216, 213. Lesquelles sont publiées ? Si la température reste ensuite à 213, à quel instant la publication suivante a-t-elle lieu ? -**Exercice 5.** Justifiez pourquoi `PublishPolicy` est dans `lib/` alors +**Exercice 5 -- Le bon dossier** (sans carte). Justifiez pourquoi `PublishPolicy` est dans `lib/` alors que `handleWifi()` est dans `src/main.cpp`. Pourquoi `shouldPublish` reçoit-elle `nowMs` en paramètre au lieu d'appeler `millis()` ? ### Solutions -**Solution 1.** Deux tâches, chacune avec son propre instant de +**Solution 1** + +Deux tâches, chacune avec son propre instant de référence : ```cpp @@ -477,7 +498,9 @@ L'initialisation `0 - READ_INTERVAL_MS` reprend l'astuce de `handleMqtt()`. Ajouter une troisième tâche ne ralentit pas les deux premières. -**Solution 2.** Le compteur a débordé entre les deux instants. Il +**Solution 2** + +Le compteur a débordé entre les deux instants. Il restait 4 294 967 296 - 4 294 965 000 = 2 296 ms avant le débordement : - `3 000 - 4 294 965 000` = 2 296 + 3 000 = **5 296** : inférieur à @@ -485,7 +508,7 @@ restait 4 294 967 296 - 4 294 965 000 = 2 296 ms avant le débordement : - `9 000 - 4 294 965 000` = 2 296 + 9 000 = **11 296** : la lecture a lieu. -**Solution 3.** +**Solution 3** 1. `true` : valeur normale, dans la plage. 2. `false` : -127, sonde absente ou CRC faux. @@ -498,13 +521,17 @@ restait 4 294 967 296 - 4 294 965 000 = 2 296 ms avant le débordement : 7. `true` : seul 85 exactement est suspect ; 84,9375 est une valeur normale (pas de 0,0625 °C à 12 bits). -**Solution 4.** À 0 s, 214 est publié (première valeur). À 10 s, 215 : +**Solution 4** + +À 0 s, 214 est publié (première valeur). À 10 s, 215 : écart de 1 avec 214, rien. À 20 s, 216 : écart de 2, publié. À 30 s, 216 : écart nul, rien. À 40 s, 213 : écart de 3 avec 216, publié. La température restant à 213, la publication suivante est le battement de -coeur, 5 minutes après la dernière : à 340 s (40 + 300). +cœur, 5 minutes après la dernière : à 340 s (40 + 300). + +**Solution 5** -**Solution 5.** `PublishPolicy` est de la logique pure : elle ne prend +`PublishPolicy` est de la logique pure : elle ne prend que des entiers et ne touche à aucun matériel. Elle compile sur PC et se teste avec des cas choisis. `handleWifi()` appelle `WiFi.status()` et `WiFi.reconnect()`, qui n'existent que sur l'ESP32 : elle ne peut vivre diff --git a/supports/modules_additionnels/module_arduino/05_tests_unitaires.md b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md index 2e46910..e47980c 100644 --- a/supports/modules_additionnels/module_arduino/05_tests_unitaires.md +++ b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md @@ -6,7 +6,7 @@ > - lancer les tests avec `pio test -e native` et lire un échec ; > - écrire un nouveau test pour un cas limite, et vérifier qu'il sait échouer. > -> **Durée de lecture** : environ 1 h 15, exercices compris. Aucune carte n'est nécessaire. +> **Durée de lecture** : environ 1 h 15, exercices compris. --- @@ -29,13 +29,13 @@ Trois exemples tirés du projet : dixièmes de degré. Faut-il publier 21,4 ou 21,5 ? Une conversion maladroite tronque au lieu d'arrondir, et l'erreur passe inaperçue sur un affichage. -- **Les cas limites.** Le DS18B20 renvoie 85 °C à la mise sous tension +- **Les cas limites.** La DS18B20 renvoie 85 °C à la mise sous tension (voir chapitre Arduino 4). Le programme doit rejeter cette valeur, sauf si la température était déjà proche de 85 °C. Pour vérifier cela sur la carte, il faudrait chauffer la sonde à 80 °C. - **Le débordement.** `millis()` revient à zéro au bout d'environ 49,7 jours (voir chapitre Arduino 4). Personne n'attend sept semaines - devant son moniteur série pour vérifier que le heartbeat survit. + devant son moniteur série pour vérifier que le battement de cœur survit. S'ajoutent les **régressions** : une modification qui casse ce qui marchait. En relançant les tests après chaque modification, on les voit @@ -175,7 +175,7 @@ Quelques remarques : limite) et 74,9 (rejeté, juste au-delà). Un bogue de frontière, `<` au lieu de `<=`, serait détecté. - **Les valeurs viennent du capteur réel** : 21,4375 et -3,0625 sont - des multiples de 1/16 de degré, la résolution du DS18B20. + des multiples de 1/16 de degré, la résolution de la DS18B20. - **Les cas pièges sont explicites.** `-0.5` est un piège classique : la partie entière de -5 / 10 vaut 0, et un formatage naïf perdrait le signe moins. @@ -275,7 +275,7 @@ Pour une fonction donnée, cherchez trois familles de cas : mais renvoie quand même (-127, 85, NaN). Prenons `PublishPolicy` (voir chapitre Arduino 4). La suite -`test_publish` vérifie déjà le heartbeat à 299999 ms et à exactement +`test_publish` vérifie déjà le battement de cœur à 299999 ms et à exactement 300000 ms après une publication. Un cas manque : après une deuxième publication, le délai doit repartir de **cette** publication, et non de la première. Ajoutez ce test dans `test/test_publish/test_main.cpp`, @@ -327,8 +327,10 @@ test/test_publish/test_main.cpp:53: test_heartbeat_restarts_from_last_publish: E ``` C'est exactement ce qu'on attend d'un nouveau test : il couvre un cas -que la suite ne voyait pas. Annulez le bogue avec `git restore`, et -gardez le test. +que la suite ne voyait pas. Annulez le bogue avec +`git restore lib/temp_logic/temp_logic.cpp`, et gardez le test le temps +de la lecture. Le `git restore` demandé à la fin de chaque exercice le +retirera aussi : les exercices repartent des 24 tests du dépôt. ## L'essentiel @@ -355,15 +357,12 @@ gardez le test. projet utilise aussi `TEST_ASSERT_EQUAL` (entier générique) et `TEST_ASSERT_GREATER_THAN` dans `test_discovery`. Pour les nombres à virgule, `TEST_ASSERT_FLOAT_WITHIN(delta, attendu, obtenu)` tolère un -écart : comparer deux `float` avec `==` est presque toujours une -erreur, à cause des arrondis binaires. Le projet contourne la question -en testant des dixièmes entiers. +écart, car comparer deux `float` avec `==` est piégeux (voir chapitre +Arduino 4). Le projet contourne la question en testant des dixièmes +entiers. -**Tester sur la carte.** PlatformIO sait aussi exécuter une suite sur -la carte : `pio test -e xiao` compile les tests pour l'ESP32-S3, les -téléverse et lit les résultats sur le port série. C'est utile pour du -code qui dépend du matériel. Le projet ne le fait pas : sa logique est -entièrement testable sur le PC, et la CI n'a pas de carte. +**Tester sur la carte.** PlatformIO sait aussi exécuter les suites sur +la carte elle-même (voir chapitre Arduino 6). **Les doublures.** Pour tester du code qui appelle le matériel (lire la sonde, publier sur MQTT), on le fait dépendre d'une interface, puis on @@ -392,33 +391,36 @@ disponibles (GoogleTest, doctest). Tous les exercices se font sans carte, dans votre clone du dépôt `arduino`, dossier `sketches/XIAO-ESP32-S3/DS18B20_MQTT`. Annulez vos -modifications avec `git restore` à la fin de chaque exercice. +modifications avec `git restore .` à la fin de chaque exercice : cela +retire aussi le test ajouté dans la section « Écrire un test ». -**Exercice 1.** Lancez `pio test -e native`. Combien de suites et +**Exercice 1 -- Lancer les tests** (sans carte). Lancez `pio test -e native`. Combien de suites et combien de cas de test sont exécutés ? Relancez seulement la suite `test_probe` avec l'option `-v` : quelles lignes Unity affiche-t-il à la fin ? -**Exercice 2.** Dans `test_reading`, ajoutez deux tests : une lecture +**Exercice 2 -- La tolérance de 85 °C** (sans carte). Dans `test_reading`, ajoutez deux tests : une lecture de 85 °C après une lecture valide de 80 °C doit être acceptée ; une lecture de 85 °C après 20 °C doit être rejetée. Vérifiez qu'ils passent. Puis, dans `temp_logic.cpp`, réduisez la tolérance `kPowerOnTolerance` de 10 à 2 degrés : quels tests échouent ? -**Exercice 3.** Le cas `formatTenths(-5)` -> `"-0.5"` est-il déjà +**Exercice 3 -- Les valeurs négatives** (sans carte). Le cas `formatTenths(-5)` -> `"-0.5"` est-il déjà testé ? Ajoutez un test pour deux autres valeurs négatives sensibles : --1 (-0,1 °C) et -550 (-55,0 °C, la limite basse du DS18B20). +-1 (-0,1 °C) et -550 (-55,0 °C, la limite basse de la DS18B20). -**Exercice 4.** Dans `ProbeHealth::record`, mettez en commentaire la +**Exercice 4 -- Un compteur non remis à zéro** (sans carte). Dans `ProbeHealth::record`, mettez en commentaire la ligne `failures_ = 0;`. Quel test échoue, et pourquoi ? Pourquoi `test_recovers_after_offline` passe-t-il quand même ? -**Exercice 5.** Proposez un cas que les 24 tests actuels ne couvrent +**Exercice 5 -- Un cas non couvert** (sans carte). Proposez un cas que les 24 tests actuels ne couvrent pas, et écrivez le test correspondant. ### Solutions -**Solution 1.** 4 suites (« Collected 4 tests ») et 24 cas de test +**Solution 1** + +4 suites (« Collected 4 tests ») et 24 cas de test (« 24 test cases: 24 succeeded »). Avec `pio test -e native -f test_probe -v`, Unity termine par : @@ -428,7 +430,9 @@ test_probe -v`, Unity termine par : OK ``` -**Solution 2.** Les deux tests, avant `main()` : +**Solution 2** + +Les deux tests, avant `main()` : ```cpp void test_85_after_80_accepted() { @@ -461,7 +465,9 @@ Le test existant échoue sur 75,0 (ligne 22), le vôtre sur 80. Le rejet après 20 °C reste juste. Le signal 2, SIGINT, correspond aux deux échecs (voir « Lire un échec »). -**Solution 3.** Oui : `test_format_tenths` le vérifie déjà. Les deux +**Solution 3** + +Oui : `test_format_tenths` le vérifie déjà. Les deux nouveaux cas : ```cpp @@ -476,7 +482,9 @@ sans oublier `RUN_TEST(test_format_small_negatives);`. Le test passe. Le tampon de 8 caractères suffit : `"-55.0"` en occupe 6 avec le caractère nul final. -**Solution 4.** Seul `test_valid_reading_resets_failure_count` échoue : +**Solution 4** + +Seul `test_valid_reading_resets_failure_count` échoue : ``` test/test_probe/test_main.cpp:40: test_valid_reading_resets_failure_count: Expected TRUE Was FALSE [FAILED] @@ -489,7 +497,9 @@ qu'elle n'a jamais échoué trois fois d'affilée. `test_recovers_after_offline` ne vérifie que le retour en ligne, qui ne dépend pas du compteur : une lecture valide met toujours `online_` à vrai. -**Solution 5.** Plusieurs réponses sont possibles. Par exemple, aucun +**Solution 5** + +Plusieurs réponses sont possibles. Par exemple, aucun test ne construit un `PublishPolicy` avec d'autres paramètres que ceux par défaut (2 dixièmes, 300000 ms) : @@ -504,6 +514,5 @@ void test_custom_parameters() { } ``` -Autres pistes : le heartbeat après une deuxième publication (l'exemple -de la section « Écrire un test »), un `ProbeHealth(5)`, ou -`formatTenths` avec un tampon trop petit. +Autres pistes : un `ProbeHealth(5)`, ou `formatTenths` avec un tampon +trop petit. diff --git a/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md index facd1f9..1ab0bda 100644 --- a/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md +++ b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md @@ -7,7 +7,7 @@ > - lire le fichier `ci.yml` du projet, ligne par ligne ; > - retrouver dans l'onglet Actions le step qui a échoué et la ligne de log qui l'explique. > -> **Durée de lecture** : ~1 h 30. +> **Durée de lecture** : environ 1 h 30, exercices compris. --- @@ -285,7 +285,9 @@ Un nouveau projet testé est donc vérifié sans toucher au workflow. changer une version dans `lib_deps` repart d'un cache vide. 3. `pip install platformio==6.1.19` fige la version : sans `==`, une nouvelle version publiée un matin pourrait casser la CI sans - qu'aucun commit ne l'explique. + qu'aucun commit ne l'explique. Cette installation par `pip`, à + version figée, convient à une machine jetable ; elle diffère de + celle du chapitre Arduino 2, prévue pour votre poste. 4. `credentials.h factice` copie le modèle aux valeurs fictives, pour que `main.cpp` compile : le vrai fichier n'est jamais versionné (voir chapitre Arduino 4). @@ -449,7 +451,7 @@ Les exercices 1 à 4 se font sans carte, dans une copie du dépôt Arduino 4). Annulez vos modifications après chaque exercice (`git restore .`). -**Exercice 1.** Introduisez deux défauts dans +**Exercice 1 -- Provoquer des défauts** (sans carte). Introduisez deux défauts dans `lib/temp_logic/temp_logic.cpp`, puis faites-les signaler par `pio check -e xiao --skip-packages --fail-on-defect=low` : @@ -466,22 +468,22 @@ size_t labelLength(std::string label) { return label.size(); } Pour chaque défaut, relevez la gravité, la famille et l'identifiant, puis corrigez-le et vérifiez que l'analyse repasse. -**Exercice 2.** Relancez l'analyse du premier défaut de l'exercice 1 +**Exercice 2 -- Sans --fail-on-defect** (sans carte). Relancez l'analyse du premier défaut de l'exercice 1 sans l'option `--fail-on-defect`. Que change le résultat ? Pourquoi la CI utilise-t-elle cette option ? -**Exercice 3.** Supprimez la ligne `// cppcheck-suppress +**Exercice 3 -- Retirer une suppression** (sans carte). Supprimez la ligne `// cppcheck-suppress constParameterCallback` de `src/main.cpp`. Prévoyez le résultat de `pio check`, puis vérifiez. Pourquoi ne corrige-t-on pas plutôt le code en ajoutant `const` ? -**Exercice 4.** Dans `sketches/common/credentials.h.example`, la +**Exercice 4 -- Un modèle incomplet** (sans carte). Dans `sketches/common/credentials.h.example`, la ligne `#define MQTT_SERVER ...` disparaît par erreur. Pour le job `sketches/XIAO-ESP32-S3/DS18B20_MQTT`, quel est le premier step qui échoue, et quel message lira-t-on dans son log ? Les tests natifs passent-ils ? -**Exercice 5.** *(Compte GitHub requis, sans carte.)* Faites un fork +**Exercice 5 -- La CI d'un fork** (sans carte, compte GitHub requis). Faites un fork du dépôt `pguinet/arduino` (voir module additionnel Git) et activez GitHub Actions dans son onglet **Actions**. Créez une branche qui casse un test, par exemple en remplaçant `85.0f` par `86.0f` dans la @@ -489,7 +491,7 @@ constante `kPowerOn` de `temp_logic.cpp`, et poussez-la. La CI se lance-t-elle ? Faites ce qu'il faut pour qu'elle se lance, puis retrouvez dans le log le test en échec. -**Exercice 6.** Votre équipe ajoute une branche `develop` : les +**Exercice 6 -- Une branche develop** (sans carte). Votre équipe ajoute une branche `develop` : les fonctionnalités y sont fusionnées par pull request, puis `develop` est fusionnée dans `main` à chaque version. On veut que la CI tourne pour les pull requests qui visent `develop` et pour chaque push sur @@ -497,7 +499,7 @@ les pull requests qui visent `develop` et pour chaque push sur ### Solutions -**Solution 1.** +**Solution 1** 1. `lib/temp_logic/temp_logic.cpp:27: [low:style] Variable 'arrondi' is assigned a value that is never used. [unreadVariable]` : gravité @@ -512,13 +514,17 @@ les pull requests qui visent `develop` et pour chaque push sur Après chaque correction, l'analyse affiche `No defects found` et `PASSED`. -**Solution 2.** Le défaut est toujours affiché, mais l'analyse se +**Solution 2** + +Le défaut est toujours affiché, mais l'analyse se termine par `PASSED` et le code de retour vaut 0. Un step GitHub Actions n'échoue que si sa commande renvoie un code non nul : sans l'option, la CI resterait verte malgré le défaut, et personne ne le lirait. -**Solution 3.** Deux défauts, un par paramètre : +**Solution 3** + +Deux défauts, un par paramètre : ``` src/main.cpp:71: [low:style] Parameter 'topic' can be declared as pointer to const. However it seems that 'onMqttMessage' is a callback function, if 'topic' is declared with const you might also need to cast function pointer(s). [constParameterCallback] @@ -531,7 +537,9 @@ type ne correspondrait plus à celui qu'attend `mqtt.setCallback`, et la compilation échouerait. La suppression en ligne, justifiée par le commentaire qui la précède, est la bonne réponse. -**Solution 4.** Le step `credentials.h factice` réussit : il copie le +**Solution 4** + +Le step `credentials.h factice` réussit : il copie le fichier, quel que soit son contenu. Les `Tests natifs` passent aussi : ils compilent `lib/temp_logic` et les tests, pas `src/main.cpp` (voir chapitre Arduino 5). Le premier step en échec est `Compilation du @@ -546,7 +554,9 @@ firmware` : `main.cpp` contient une garde et le log du step affiche ce message d'erreur. Le step `cppcheck` est sauté. -**Solution 5.** La CI ne se lance pas : `on.push` ne vise que `main`, +**Solution 5** + +La CI ne se lance pas : `on.push` ne vise que `main`, et aucune pull request n'existe encore. Il faut ouvrir une pull request de la branche vers `main` dans le fork (attention à choisir le fork, et non `pguinet/arduino`, comme dépôt de base). Le run démarre ; @@ -564,7 +574,9 @@ de mise sous tension : elle est acceptée alors que les tests attendent un rejet. Les steps suivants sont sautés ; l'autre projet de la matrice continue grâce à `fail-fast: false`. -**Solution 6.** Pour les pull requests, rien : `pull_request:` sans +**Solution 6** + +Pour les pull requests, rien : `pull_request:` sans filtre couvre déjà toutes les branches visées, `develop` comprise. Pour les push, il faut ajouter `develop` à la liste : From 54975ceda726c0f4403dd626c82f89a0f81c6cc7 Mon Sep 17 00:00:00 2001 From: Pascal GUINET Date: Thu, 15 Oct 2026 22:31:24 +0200 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20tirets=20des=20titres=20d'exercice?= =?UTF-8?q?s=20et=20d=C3=A9bordements=20du=20PDF=20Arduino?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../2026-10-15-module-arduino.md.tasks.json | 18 +++++++++--------- .../module_arduino/01_arduino_xiao_esp32s3.md | 10 +++++----- .../module_arduino/02_ide_vers_platformio.md | 18 +++++++++--------- .../module_arduino/03_mqtt_home_assistant.md | 10 +++++----- .../module_arduino/04_lecture_code.md | 10 +++++----- .../module_arduino/05_tests_unitaires.md | 10 +++++----- .../06_qualite_integration_continue.md | 15 ++++++++------- 7 files changed, 46 insertions(+), 45 deletions(-) diff --git a/docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json b/docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json index b31eaf5..eba834e 100644 --- a/docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json +++ b/docs/superpowers/plans/2026-10-15-module-arduino.md.tasks.json @@ -1,14 +1,14 @@ { "planPath": "docs/superpowers/plans/2026-10-15-module-arduino.md", "tasks": [ - {"id": 0, "subject": "Task 0: Catalogue PDF et squelettes", "status": "pending"}, - {"id": 1, "subject": "Task 1: Chapitre Arduino 1", "status": "pending", "blockedBy": [0]}, - {"id": 2, "subject": "Task 2: Chapitre Arduino 2", "status": "pending", "blockedBy": [0]}, - {"id": 3, "subject": "Task 3: Chapitre Arduino 3", "status": "pending", "blockedBy": [0]}, - {"id": 4, "subject": "Task 4: Chapitre Arduino 4", "status": "pending", "blockedBy": [0]}, - {"id": 5, "subject": "Task 5: Chapitre Arduino 5", "status": "pending", "blockedBy": [0]}, - {"id": 6, "subject": "Task 6: Chapitre Arduino 6", "status": "pending", "blockedBy": [0]}, - {"id": 7, "subject": "Task 7: Documentation du dépôt et vérification finale", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6]} + {"id": 0, "subject": "Task 0: Catalogue PDF et squelettes", "status": "completed"}, + {"id": 1, "subject": "Task 1: Chapitre Arduino 1", "status": "completed", "blockedBy": [0]}, + {"id": 2, "subject": "Task 2: Chapitre Arduino 2", "status": "completed", "blockedBy": [0]}, + {"id": 3, "subject": "Task 3: Chapitre Arduino 3", "status": "completed", "blockedBy": [0]}, + {"id": 4, "subject": "Task 4: Chapitre Arduino 4", "status": "completed", "blockedBy": [0]}, + {"id": 5, "subject": "Task 5: Chapitre Arduino 5", "status": "completed", "blockedBy": [0]}, + {"id": 6, "subject": "Task 6: Chapitre Arduino 6", "status": "completed", "blockedBy": [0]}, + {"id": 7, "subject": "Task 7: Documentation du dépôt et vérification finale", "status": "completed", "blockedBy": [1, 2, 3, 4, 5, 6]} ], - "lastUpdated": "2026-10-15T00:00:00Z" + "lastUpdated": "2026-10-15T20:31:23Z" } diff --git a/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md index 8acc42f..be8c04e 100644 --- a/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md +++ b/supports/modules_additionnels/module_arduino/01_arduino_xiao_esp32s3.md @@ -326,22 +326,22 @@ bien des suppositions. ## Exercices -**Exercice 1 -- Trouver la carte** (avec la carte). Branchez la XIAO ESP32-S3 sur votre PC +**Exercice 1 — Trouver la carte** (avec la carte). Branchez la XIAO ESP32-S3 sur votre PC Linux. Quelles commandes permettent de trouver le nom du port série et de vérifier que le PC voit une puce ESP32-S3 ? Comment distinguer la XIAO d'une autre carte ESP32-S3 ? -**Exercice 2 -- Sans résistance de tirage** (sans carte). Un camarade a câblé une sonde DS18B20 sur +**Exercice 2 — Sans résistance de tirage** (sans carte). Un camarade a câblé une sonde DS18B20 sur une XIAO comme dans le tableau de câblage, mais il a supprimé la ligne `pinMode(ONE_WIRE_PIN, INPUT_PULLUP);` de `setup()`. Il n'a pas soudé de résistance externe. Que va-t-il observer, et pourquoi ? Proposez deux corrections. -**Exercice 3 -- Pas de WiFi** (sans carte). Votre XIAO affiche en boucle +**Exercice 3 — Pas de WiFi** (sans carte). Votre XIAO affiche en boucle `WiFi toujours absent (statut 1) : relance`, alors que le nom du réseau est correct. Quelle est la cause la plus probable ? -**Exercice 4 -- Le bon fichier** (sans carte). Pour chaque besoin, indiquez le fichier ou +**Exercice 4 — Le bon fichier** (sans carte). Pour chaque besoin, indiquez le fichier ou le dossier du projet concerné : 1. changer la broche à laquelle la sonde est reliée ; @@ -350,7 +350,7 @@ le dossier du projet concerné : 4. imprimer un boîtier ; 5. renseigner le mot de passe du WiFi. -**Exercice 5 -- Cloner le projet** (sans carte). Clonez le dépôt du projet, placez-vous au +**Exercice 5 — Cloner le projet** (sans carte). Clonez le dépôt du projet, placez-vous au commit `1af068f` utilisé dans tout le module, et vérifiez que le dossier du projet contient bien les fichiers listés dans ce chapitre. diff --git a/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md index 5ef0104..fc087fc 100644 --- a/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md +++ b/supports/modules_additionnels/module_arduino/02_ide_vers_platformio.md @@ -112,7 +112,7 @@ PlatformIO tourne sous Linux, Windows et macOS. Au choix : - **En ligne de commande seule.** La documentation officielle recommande son script d'installation : - ```bash + ``` curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py python3 get-platformio.py ``` @@ -153,7 +153,7 @@ module additionnel Git). `DS18B20_MQTT` n'a pas de dossier `include/`. Voici celui du projet, en entier ([source](https://github.com/pguinet/arduino/blob/1af068f/sketches/XIAO-ESP32-S3/DS18B20_MQTT/platformio.ini)) : -```ini +``` ; DS18B20_MQTT - XIAO ESP32-S3 ; Sonde DS18B20 publiee dans Home Assistant (MQTT Discovery) @@ -330,7 +330,7 @@ pas écrire sur `/dev/ttyACM0` (voir chapitre Arduino 1). PlatformIO fournit des **règles udev** qui ouvrent l'accès aux cartes connues, dont `303a:1001` : -```bash +``` curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules sudo udevadm control --reload-rules sudo udevadm trigger @@ -404,7 +404,7 @@ chaque commande. ## Exercices -**Exercice 1 -- Porter un Blink** (sans carte). Créez un projet vide +**Exercice 1 — Porter un Blink** (sans carte). Créez un projet vide dans un nouveau dossier : ```bash @@ -417,7 +417,7 @@ projet `DS18B20_MQTT`. Copiez le Blink du début du chapitre dans `src/main.cpp`, compilez avec `pio run`, puis corrigez le fichier jusqu'à ce que la compilation réussisse. -**Exercice 2 -- Ajouter une bibliothèque** (sans carte). Dans le projet +**Exercice 2 — Ajouter une bibliothèque** (sans carte). Dans le projet de l'exercice 1, déclarez `bblanchon/ArduinoJson@^7.0.0` dans `lib_deps`. Écrivez un `main.cpp` qui construit un `JsonDocument` contenant une température et l'écrit sur `Serial` toutes les 2 @@ -425,17 +425,17 @@ secondes, puis compilez. Où la bibliothèque a-t-elle été installée ? Quelle ligne faut-il aussi ajouter pour lire ce message avec `pio device monitor` ? -**Exercice 3 -- Contraintes de version** (sans carte). Pour chaque ligne de +**Exercice 3 — Contraintes de version** (sans carte). Pour chaque ligne de `lib_deps` du projet, donnez la plus petite et la plus grande version acceptées. La version 3.0.0 de `OneWire` serait-elle installée ? Et `PubSubClient` 2.8.1 ? -**Exercice 4 -- Les environnements** (sans carte). Dans le projet +**Exercice 4 — Les environnements** (sans carte). Dans le projet `DS18B20_MQTT`, quels sont les environnements ? Que compile `pio run` sans option ? Que se passe-t-il avec `pio run -e native`, et pourquoi cet environnement sert-il malgré tout ? -**Exercice 5 -- Reproductibilité** (sans carte). Un collègue vous envoie un sketch +**Exercice 5 — Reproductibilité** (sans carte). Un collègue vous envoie un sketch `.ino` qui « marche chez lui ». Listez les informations qui vous manquent pour le compiler à l'identique, et indiquez où chacune se trouverait dans un projet PlatformIO. @@ -451,7 +451,7 @@ installation. Si la commande se termine par `IncompatiblePlatform` (voir « Pour aller plus loin »), les fichiers sont tout de même créés. Dans tous les cas, on remplace cette ligne par : -```ini +``` platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip ``` diff --git a/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md index 657c36b..460a606 100644 --- a/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md +++ b/supports/modules_additionnels/module_arduino/03_mqtt_home_assistant.md @@ -329,12 +329,12 @@ de chacun. Tous les exercices se font sans carte. L'exercice 5 demande Docker. -**Exercice 1 -- Les messages retenus** (sans carte). La carte fonctionne depuis une heure. Un nouveau client +**Exercice 1 — Les messages retenus** (sans carte). La carte fonctionne depuis une heure. Un nouveau client s'abonne à `xiao_ds18b20/#` et à `homeassistant/#`. Que reçoit-il immédiatement, avant toute nouvelle publication ? Et si la carte a été débranchée il y a dix minutes ? -**Exercice 2 -- Écrire des filtres** (sans carte). Plusieurs cartes publient sur le même broker. Écrivez : +**Exercice 2 — Écrire des filtres** (sans carte). Plusieurs cartes publient sur le même broker. Écrivez : 1. le filtre qui reçoit la température de toutes les cartes, et rien d'autre ; @@ -343,19 +343,19 @@ débranchée il y a dix minutes ? 3. le filtre qui reçoit les messages de découverte de tous les capteurs annoncés à Home Assistant. -**Exercice 3 -- Retenir ou non** (sans carte). Pourquoi la température n'est-elle pas publiée en message +**Exercice 3 — Retenir ou non** (sans carte). Pourquoi la température n'est-elle pas publiée en message retenu, alors que `status` et `probe` le sont ? Décrivez ce que verrait Home Assistant, au redémarrage, si elle l'était et que la carte était hors service. -**Exercice 4 -- La disponibilité** (sans carte). Dans chaque situation, dites ce que valent `status` et +**Exercice 4 — La disponibilité** (sans carte). Dans chaque situation, dites ce que valent `status` et `probe`, et ce qu'affiche Home Assistant : 1. tout fonctionne ; 2. le fil DATA de la sonde est coupé, la carte reste connectée ; 3. la carte n'est plus alimentée. -**Exercice 5 -- Un broker local** (sans carte, avec Docker). Simulez la carte avec un broker Mosquitto local +**Exercice 5 — Un broker local** (sans carte, avec Docker). Simulez la carte avec un broker Mosquitto local et les outils `mosquitto_pub` et `mosquitto_sub` fournis dans l'image : 1. lancez le broker `eclipse-mosquitto:2` en autorisant les connexions diff --git a/supports/modules_additionnels/module_arduino/04_lecture_code.md b/supports/modules_additionnels/module_arduino/04_lecture_code.md index a29396e..ca8fbf4 100644 --- a/supports/modules_additionnels/module_arduino/04_lecture_code.md +++ b/supports/modules_additionnels/module_arduino/04_lecture_code.md @@ -406,7 +406,7 @@ MQTT asynchrone. Aucun exercice ne demande la carte. L'exercice 1 se vérifie avec `pio run` sur un projet PlatformIO vide (voir chapitre Arduino 2). -**Exercice 1 -- Sans delay()** (sans carte). Ce sketch fait clignoter la LED et affiche une mesure +**Exercice 1 — Sans delay()** (sans carte). Ce sketch fait clignoter la LED et affiche une mesure analogique, mais la LED ne change d'état que toutes les 10 s : ```cpp @@ -429,12 +429,12 @@ Réécrivez-le sans `delay()` long : la LED change d'état toutes les démarrage. Suivez le style du projet (une fonction `handle*()` par tâche). -**Exercice 2 -- Le débordement** (sans carte). Une lecture a eu lieu à `lastRead = 4 294 965 000`. +**Exercice 2 — Le débordement** (sans carte). Une lecture a eu lieu à `lastRead = 4 294 965 000`. L'intervalle est de 10 000 ms. Calculez `millis() - lastRead` pour `millis() = 3 000`, puis `millis() = 9 000`. Dans chaque cas, la lecture suivante a-t-elle lieu ? -**Exercice 3 -- Valider une lecture** (sans carte). Pour chaque appel, dites ce que renvoie +**Exercice 3 — Valider une lecture** (sans carte). Pour chaque appel, dites ce que renvoie `isValidReading(celsius, hasPrevious, previous)` et pourquoi : 1. `(21.5, false, 0)` @@ -445,12 +445,12 @@ lecture suivante a-t-elle lieu ? 6. `(125.5, true, 20)` 7. `(84.9375, false, 0)` -**Exercice 4 -- Quand publier** (sans carte). Une `PublishPolicy` par défaut reçoit, toutes les 10 s +**Exercice 4 — Quand publier** (sans carte). Une `PublishPolicy` par défaut reçoit, toutes les 10 s à partir de `t = 0`, les valeurs en dixièmes 214, 215, 216, 216, 213. Lesquelles sont publiées ? Si la température reste ensuite à 213, à quel instant la publication suivante a-t-elle lieu ? -**Exercice 5 -- Le bon dossier** (sans carte). Justifiez pourquoi `PublishPolicy` est dans `lib/` alors +**Exercice 5 — Le bon dossier** (sans carte). Justifiez pourquoi `PublishPolicy` est dans `lib/` alors que `handleWifi()` est dans `src/main.cpp`. Pourquoi `shouldPublish` reçoit-elle `nowMs` en paramètre au lieu d'appeler `millis()` ? diff --git a/supports/modules_additionnels/module_arduino/05_tests_unitaires.md b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md index e47980c..796bca2 100644 --- a/supports/modules_additionnels/module_arduino/05_tests_unitaires.md +++ b/supports/modules_additionnels/module_arduino/05_tests_unitaires.md @@ -394,26 +394,26 @@ Tous les exercices se font sans carte, dans votre clone du dépôt modifications avec `git restore .` à la fin de chaque exercice : cela retire aussi le test ajouté dans la section « Écrire un test ». -**Exercice 1 -- Lancer les tests** (sans carte). Lancez `pio test -e native`. Combien de suites et +**Exercice 1 — Lancer les tests** (sans carte). Lancez `pio test -e native`. Combien de suites et combien de cas de test sont exécutés ? Relancez seulement la suite `test_probe` avec l'option `-v` : quelles lignes Unity affiche-t-il à la fin ? -**Exercice 2 -- La tolérance de 85 °C** (sans carte). Dans `test_reading`, ajoutez deux tests : une lecture +**Exercice 2 — La tolérance de 85 °C** (sans carte). Dans `test_reading`, ajoutez deux tests : une lecture de 85 °C après une lecture valide de 80 °C doit être acceptée ; une lecture de 85 °C après 20 °C doit être rejetée. Vérifiez qu'ils passent. Puis, dans `temp_logic.cpp`, réduisez la tolérance `kPowerOnTolerance` de 10 à 2 degrés : quels tests échouent ? -**Exercice 3 -- Les valeurs négatives** (sans carte). Le cas `formatTenths(-5)` -> `"-0.5"` est-il déjà +**Exercice 3 — Les valeurs négatives** (sans carte). Le cas `formatTenths(-5)` -> `"-0.5"` est-il déjà testé ? Ajoutez un test pour deux autres valeurs négatives sensibles : -1 (-0,1 °C) et -550 (-55,0 °C, la limite basse de la DS18B20). -**Exercice 4 -- Un compteur non remis à zéro** (sans carte). Dans `ProbeHealth::record`, mettez en commentaire la +**Exercice 4 — Un compteur non remis à zéro** (sans carte). Dans `ProbeHealth::record`, mettez en commentaire la ligne `failures_ = 0;`. Quel test échoue, et pourquoi ? Pourquoi `test_recovers_after_offline` passe-t-il quand même ? -**Exercice 5 -- Un cas non couvert** (sans carte). Proposez un cas que les 24 tests actuels ne couvrent +**Exercice 5 — Un cas non couvert** (sans carte). Proposez un cas que les 24 tests actuels ne couvrent pas, et écrivez le test correspondant. ### Solutions diff --git a/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md index 1ab0bda..85cf59b 100644 --- a/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md +++ b/supports/modules_additionnels/module_arduino/06_qualite_integration_continue.md @@ -264,7 +264,8 @@ Un nouveau projet testé est donc vérifié sans toucher au workflow. ### Le job platformio : une matrice de projets - `needs: discover` attend la fin de `discover` et donne accès à ses - sorties (`needs.discover.outputs.projects`). + sorties : la liste des projets est lue par + `needs.discover.outputs.projects`. - `strategy.matrix` crée **une copie du job par valeur** de la liste. `fromJSON(...)` convertit la chaîne JSON en vraie liste : la matrice est dynamique. Avec deux projets, on obtient deux jobs en parallèle, @@ -451,7 +452,7 @@ Les exercices 1 à 4 se font sans carte, dans une copie du dépôt Arduino 4). Annulez vos modifications après chaque exercice (`git restore .`). -**Exercice 1 -- Provoquer des défauts** (sans carte). Introduisez deux défauts dans +**Exercice 1 — Provoquer des défauts** (sans carte). Introduisez deux défauts dans `lib/temp_logic/temp_logic.cpp`, puis faites-les signaler par `pio check -e xiao --skip-packages --fail-on-defect=low` : @@ -468,22 +469,22 @@ size_t labelLength(std::string label) { return label.size(); } Pour chaque défaut, relevez la gravité, la famille et l'identifiant, puis corrigez-le et vérifiez que l'analyse repasse. -**Exercice 2 -- Sans --fail-on-defect** (sans carte). Relancez l'analyse du premier défaut de l'exercice 1 +**Exercice 2 — Sans `--fail-on-defect`** (sans carte). Relancez l'analyse du premier défaut de l'exercice 1 sans l'option `--fail-on-defect`. Que change le résultat ? Pourquoi la CI utilise-t-elle cette option ? -**Exercice 3 -- Retirer une suppression** (sans carte). Supprimez la ligne `// cppcheck-suppress +**Exercice 3 — Retirer une suppression** (sans carte). Supprimez la ligne `// cppcheck-suppress constParameterCallback` de `src/main.cpp`. Prévoyez le résultat de `pio check`, puis vérifiez. Pourquoi ne corrige-t-on pas plutôt le code en ajoutant `const` ? -**Exercice 4 -- Un modèle incomplet** (sans carte). Dans `sketches/common/credentials.h.example`, la +**Exercice 4 — Un modèle incomplet** (sans carte). Dans `sketches/common/credentials.h.example`, la ligne `#define MQTT_SERVER ...` disparaît par erreur. Pour le job `sketches/XIAO-ESP32-S3/DS18B20_MQTT`, quel est le premier step qui échoue, et quel message lira-t-on dans son log ? Les tests natifs passent-ils ? -**Exercice 5 -- La CI d'un fork** (sans carte, compte GitHub requis). Faites un fork +**Exercice 5 — La CI d'un fork** (sans carte, compte GitHub requis). Faites un fork du dépôt `pguinet/arduino` (voir module additionnel Git) et activez GitHub Actions dans son onglet **Actions**. Créez une branche qui casse un test, par exemple en remplaçant `85.0f` par `86.0f` dans la @@ -491,7 +492,7 @@ constante `kPowerOn` de `temp_logic.cpp`, et poussez-la. La CI se lance-t-elle ? Faites ce qu'il faut pour qu'elle se lance, puis retrouvez dans le log le test en échec. -**Exercice 6 -- Une branche develop** (sans carte). Votre équipe ajoute une branche `develop` : les +**Exercice 6 — Une branche develop** (sans carte). Votre équipe ajoute une branche `develop` : les fonctionnalités y sont fusionnées par pull request, puis `develop` est fusionnée dans `main` à chaque version. On veut que la CI tourne pour les pull requests qui visent `develop` et pour chaque push sur