Télécharger le projet, les exercices et les corrections (ZIP, v0.2.2)
Leçon 3 Conserver les mesures pendant une coupure

Dans la leçon précédente, le programme abandonnait les mesures qu’il produisait en se sachant hors connexion. Nous allons maintenant les conserver en RAM et reprendre les envois quand MQTT redevient disponible.
La correction répond à ce problème précis. Elle ne protège pas les données d’un reset et ne prouve pas leur réception finale. Nous garderons le QoS 0 pour comparer les deux modes sans changer en même temps la politique de stockage et le protocole d’envoi.
Choisir ce que l’on conserve
Toutes les mesures n’ont pas le même usage. Pour afficher la température actuelle, une vieille valeur peut avoir peu d’intérêt. Pour reconstituer un événement, perdre l’historique peut empêcher de comprendre ce qui s’est passé.
| Politique | Usage possible | Conséquence pendant une longue coupure |
|---|---|---|
| Conserver seulement la dernière valeur | Affichage d’un état récent | Les états intermédiaires disparaissent. |
| Conserver les premières valeurs jusqu’à la capacité disponible | Reconstituer le début d’un incident | Les nouvelles valeurs sont rejetées quand le tampon est plein. |
| Remplacer les plus anciennes valeurs | Garder l’historique le plus récent | Le début de l’incident disparaît. |
| Abandonner et signaler les pertes | Donnée peu importante ou mémoire très limitée | L’historique est incomplet, avec une perte explicitement comptée. |
Pour cet exercice, nous choisissons la deuxième politique : une file de 64 mesures, les plus anciennes en tête, avec rejet des nouvelles lorsque la file est pleine. Ce choix permet d’étudier un historique conservé et une saturation visible. Il n’est pas préférable pour tous les produits.
Lire la correction dans le code
Ouvrez main/backlog.c. pvt_backlog_offer copie une mesure dans un emplacement libre. Si count atteint capacity, la fonction renvoie false sans écraser les éléments déjà présents.
if (q->count == q->capacity) return false;
q->items[(q->head + q->count) % q->capacity] = *sample;
++q->count;
head désigne le plus ancien élément. Le modulo ramène l’indice au début du tableau lorsque l’on atteint sa fin. Nous réutilisons les emplacements libérés sans déplacer les mesures restantes.
Dans main/main.c, le producteur propose désormais chaque mesure au tampon, même hors connexion. L’expéditeur consulte la tête avec pvt_backlog_peek, fait une copie locale puis libère le verrou avant d’appeler MQTT. Le producteur peut continuer à remplir les autres emplacements pendant que cet appel attend.
Si la publication échoue, la tête reste dans le tampon ; une nouvelle tentative peut suivre. Si elle revient avec un résultat positif ou nul, pvt_backlog_commit retire cette tête. Le code vérifie son numéro pour éviter de retirer une autre mesure. Un seul expéditeur retire les éléments.
Le verrou protège les opérations courtes sur les indices et les copies. Il n’entoure ni la publication réseau ni les logs. La capacité inclut la mesure en cours de tentative : elle occupe encore sa place jusqu’au retrait.
Le point de retrait compte. Avec le QoS 0 de ce laboratoire, retired_after_publish signifie que l’appel a signalé un succès. Il ne signifie pas que le récepteur a reçu ni stocké la valeur. Une coupure au mauvais moment peut encore laisser une absence. Inversement, une tentative peut avoir atteint le récepteur avant qu’un échec soit signalé ; sa répétition peut alors produire un doublon.
Rejouer la même coupure
Reprenez les paramètres de la leçon 2 : 60 mesures, 1000 ms, coupure après 15 secondes pendant 30 secondes. Dans menuconfig, activez Keep unsent samples in a bounded RAM buffer (lesson 3) et laissez RAM buffer capacity in samples à 64.
idf.py build
idf.py -p PORT flash
Ouvrez un nouveau fichier de réception :
python tools/capture_mqtt.py --host 192.168.1.10 --out captures/outage-buffer64.jsonl
Attendez READY, ouvrez le moniteur, activez le log et réinitialisez la carte comme précédemment. Conservez le broker et le récepteur actifs.
Pendant la coupure, cherchez les événements buffered et les lignes BACKLOG depth=.... La profondeur doit augmenter tant que les mesures ne peuvent pas être envoyées. Au retour de MQTT, les premières mesures en attente sont tentées avant les plus récentes.
Attendez PRODUCTION_DONE et DRAIN_DONE, puis encore dix secondes avant d’arrêter les captures. La première ligne termine la production ; la seconde indique que le tampon local est vide. Si le réseau ne revient pas, le tampon peut rester occupé et DRAIN_DONE ne viendra pas. Gardez les logs et corrigez la connexion au lieu de réinitialiser la carte sans relever cet état.
python tools/compare.py --serial SERIAL_FILE --received captures/outage-buffer64.jsonl
Avec une capture complète, aucune saturation et des publications effectivement reçues, l’objectif est de retrouver les 60 identités, sans contenu modifié. Confrontez cet objectif au résultat réel : un tampon vide ne suffit pas à conclure.
Remplissez votre tableau avec les essais de la leçon 2 et de cette leçon. Chaque ligne correspond à une exécution complète ; son boot est différent.
| Essai | Produites | Identités communes | Absentes | Doublons | Rejets locaux |
|---|---|---|---|---|---|
| Sans conservation | À relever | À relever | À relever | À relever | dropped_offline, dropped_busy |
| Tampon 64 | À relever | À relever | À relever | À relever | dropped_full |
Observer le rattrapage
Les anciennes mesures peuvent arriver très vite les unes après les autres quand la connexion revient. Leurs uptime_ms doivent pourtant conserver la cadence de production initiale. Nous envoyons les mesures d’origine, sans remplacer leur horodatage par celui de l’envoi.
Repérez deux mesures consécutives créées pendant la coupure. Comparez leur écart d’uptime_ms à leur écart de réception dans le fichier JSONL. Le premier décrit la production sur la carte ; le second décrit l’arrivée sur l’ordinateur. Ne soustrayez pas directement une date UTC du récepteur et un uptime de l’ESP32 : ils n’ont pas la même origine.
Réduire la capacité pour voir la saturation
Passez la capacité à 8, sans changer la cadence ni la coupure. Recompilez, flashez et recommencez avec captures/outage-buffer8.jsonl. Prévoyez quels numéros seront gardés avant de regarder les résultats.
La profondeur ne doit jamais dépasser 8. Lorsque le tampon est plein, le programme écrit dropped_full pour chaque nouvelle mesure refusée. Au retour du réseau, il reprend les anciens éléments conservés. Comparez les rejets aux numéros absents, sans supposer que tous les autres messages sont forcément arrivés.
Voici un calcul de modèle, pas une mesure ESP32 : si la connexion est indisponible exactement pour les numéros 16 à 45, cela représente 30 mesures. Un tampon vide de huit places conserve 16 à 23 et rejette 24 à 45, soit 22 mesures. Si tous les autres envois aboutissent, 38 identités arrivent sur 60. Un tampon de 64 places peut conserver les trente. Le test C fourni vérifie ce scénario déterministe ; les frontières et les temps de reprise sur carte doivent être mesurés.
Dimensionner sans oublier la reprise
À une mesure par seconde, trente secondes d’indisponibilité produisent environ trente mesures. Prévoyez aussi les éléments déjà en attente, le temps de reconnexion et une marge pour les frontières de cadence. La mémoire des éléments vaut capacity * sizeof(pvt_sample_t) ; le log de démarrage donne le nombre réel d’octets pour la cible compilée. S’y ajoutent le contrôle du tampon, le verrou, les piles et les ressources MQTT.
Un tampon ne se vide durablement que si le débit d’envoi effectif dépasse la production pendant le rattrapage. Avec un excédent de quatre mesures envoyées par seconde et trente en attente, le rattrapage prend au moins 7,5 secondes dans un modèle à débit constant. Changer la taille du tableau ne résout pas un débit durablement insuffisant.
Exercice et correction
Une application produit deux mesures par seconde. Vous voulez couvrir quarante secondes sans envoi, plus dix secondes de reconnexion, avec quatre mesures déjà en attente. Quelle capacité minimale donne ce calcul ? Une capacité de 64 suffit-elle ? Que se passe-t-il si l’ESP32 redémarre ?
Correction. Le calcul donne 2 * (40 + 10) + 4 = 104 places, avant marge sur la cadence et l’ordonnancement. Une capacité de 64 est insuffisante. Un reset perd les éléments présents en RAM ; boot change et permet de distinguer les nouvelles identités, mais ne restaure rien.
Pour vérifier le comportement de notre code, provoquez un reset pendant une coupure, après avoir observé un tampon non vide. Le récepteur verra un nouveau boot au redémarrage ; les anciennes mesures restées uniquement en RAM ne seront pas rejouées. Utilisez de nouveaux fichiers et analysez les deux sessions séparément.
Enfin, expliquez si passer au QoS 1 supprimerait toutes les absences. Réponse : un accusé MQTT du broker change les garanties entre client et broker et peut impliquer des retransmissions. Il ne prouve pas le traitement par l’application abonnée, ne sauvegarde pas automatiquement notre tampon après un reset et ne dispense pas de traiter les doublons. Une garantie de traitement demande de définir un accusé applicatif, le stockage associé et le point de retrait. retain conserve une dernière valeur par topic au broker ; ce n’est pas une file historique de toutes les mesures.
Terminer le parcours
Votre résultat est un ensemble de captures accompagné d’une explication : ce qui a été conservé, abandonné ou seulement tenté, et pourquoi. Gardez les réglages avec chaque essai. Vous devez pouvoir justifier une capacité, choisir une politique de saturation et distinguer le retour d’un appel réseau de la réception observée.
La correction complète se trouve dans main/backlog.c et dans les branches CONFIG_PVT_BUFFERED de main/main.c. Désactivez l’option pour revenir au point de départ. Le laboratoire reste sur un réseau local isolé avec des données fictives ; le cours gratuit suivant doit traiter authentification, droits d’accès et TLS avant un usage sur un réseau exposé.
Références techniques
Le code ESP-MQTT épinglé avec ESP-IDF 5.5.5 décrit le retour de publication. La spécification MQTT 3.1.1, sections 3.3.1 et 4.3, précise le fonctionnement de retain et des niveaux de QoS.