Débitmètre Raspberry Pi — compter des impulsions sans jamais rien écrire
Un capteur à effet Hall YF-S201 sur un Raspberry Pi, une interruption GPIO par tour de rotor, et deux défauts dans dix lignes qui rendaient la mesure fausse puis muette. Relecture six ans après.
Technologies employées
- Raspberry Pi 3B
- YF-S201
- Python 3
- RPi.GPIO
- Supervisor
- Linux
- Bash
Fin avril 2020, j’ai voulu savoir combien d’eau passait réellement dans une canalisation. Un capteur à quelques euros, un Raspberry Pi, une trentaine de lignes de Python : sur le papier, l’affaire d’une soirée. Le montage a fonctionné, les impulsions sont bien arrivées, et le fichier de mesures est resté vide. En relisant ce code six ans plus tard, j’y trouve deux défauts que je n’avais pas vus à l’époque — l’un rendait la mesure fausse d’un facteur 60, l’autre l’empêchait purement et simplement d’être enregistrée.
Le capteur et le principe
Un YF-S201 est un boîtier en plastique traversé par un rotor à aimant, avec un capteur à effet Hall collé contre la paroi. L’eau fait tourner le rotor, l’aimant passe devant le capteur, et celui-ci commute. On n’obtient pas un débit : on obtient un train d’impulsions dont la fréquence est proportionnelle au débit.

Le capteur est donné pour 1 à 30 litres par minute, sous 1,75 MPa au maximum, et sa constante vaut 7,5 : 7,5 impulsions par seconde pour un litre par minute. Toute la mesure tient dans cette phrase, et c’est précisément elle que mon code a mal traduite.
Un DHT11 figure aussi sur le schéma, pour relever température et humidité. Il n’a jamais été lu : la ligne correspondante est restée commentée. C’est un détail — mais il explique le second défaut.
La chaîne de mesure
Rien n’est interrogé : tout part d’une interruption matérielle. Le programme ne demande jamais au capteur où il en est, c’est le capteur qui réveille le programme à chaque tour.
- rotorl’eau le fait tourner
- effet Hallune impulsion par tour
- GPIO 17front montant
- callbackcompteur + 1
- toutes les 2 scalcul et écriture
Le calcul et l’écriture du fichier ont lieu DANS la fonction d’interruption — c’est l’origine des ennuis.
Ce choix-là était bon. GPIO.add_event_detect sur front montant coûte
infiniment moins qu’une boucle qui relit une broche des milliers de fois par
seconde, et le programme principal peut dormir. La numérotation était juste elle
aussi : broche physique 11, GPIO.setmode(GPIO.BCM), broche 17. C’est cohérent,
et c’est le genre de détail où l’on se trompe une fois sur deux.
Ce qui vient après l’est beaucoup moins.
Deux défauts en dix lignes
tdelta = (time.time() - time_start)
if tdelta >= 2:
flow = (pulseCount / calibrationFactor) / (tdelta * 60)
flow = round(flow, 2)
fichier = open("/home/pi/mesuresDebit.log", "a")
fichier.write("%s,%s,%s,%s\n" % (time.strftime('%a %H:%M:%S'), flow))
fichier.close()Le calcul est faux d’un facteur 60. La constante du capteur relie une fréquence à un débit : le débit en litres par minute vaut le nombre d’impulsions par seconde divisé par 7,5. Il fallait donc diviser le compteur par la durée écoulée, puis par 7,5. J’ai divisé par 7,5, puis par la durée multipliée par 60 — converti des minutes en secondes dans le mauvais sens, et ajouté une division par la durée là où il n’en fallait pas.
Sur un débit réel de 10 L/min, le capteur émet 75 impulsions par seconde, soit 150 sur les deux secondes de la fenêtre. La ligne écrite annonce alors 0,17 L/min. Le rapport est constant : la mesure est 60 fois trop basse, toujours.
Rien n’est jamais écrit. La chaîne de format porte quatre %s et ne reçoit
que deux valeurs. Python lève TypeError: not enough arguments for format string — et la lève dans le fil d’exécution de l’interruption, où RPi.GPIO
l’affiche sans arrêter le programme. Le processus continue donc de tourner,
l’affichage à l’écran continue de défiler, et mesuresDebit.log reste vide.
L’origine du défaut est visible quelques lignes plus haut dans le fichier : la lecture du DHT11 est commentée. Les quatre emplacements attendaient l’horodatage, le débit, l’humidité et la température. En retirant la lecture des deux dernières valeurs, j’ai retiré les arguments sans retirer leurs emplacements.
| Défaut | Conséquence observable | Ce qu’il fallait écrire |
|---|---|---|
| Conversion de fréquence inversée | Débit annoncé 60 fois trop bas, systématiquement | (pulseCount / tdelta) / 7.5 |
Quatre %s pour deux valeurs | TypeError à chaque cycle, fichier de mesures vide | Deux emplacements, ou remettre les deux mesures |
| Ouverture du fichier dans l’interruption | Écriture disque à la cadence du capteur, compteur exposé | Accumuler, et écrire depuis la boucle principale |
Le troisième point du tableau n’est pas un bug, mais il en aurait produit : ouvrir, écrire et fermer un fichier depuis une fonction d’interruption place une entrée-sortie disque sur le chemin critique du capteur. À trente litres par minute, le front montant arrive 225 fois par seconde. L’incrément et sa remise à zéro ne sont pas davantage protégés : rien ne garantit qu’aucune impulsion ne tombe entre le calcul et la remise à zéro du compteur.
Ce que Supervisor apportait
La seconde moitié du dépôt ne parle plus du capteur, mais de sa disponibilité : un programme de mesure qui s’arrête et que personne ne relance ne mesure rien.
Supervisor démarre le script au démarrage de la machine et le relance en cas
d’arrêt inattendu. Au-dessus, un script Python interroge supervisorctl status
toutes les cinq secondes, le compare à un état écrit dans un fichier, et corrige
l’écart. Un état désiré, un état constaté, une boucle qui les rapproche : c’est
de la réconciliation, six ans avant que je ne mette ce mot dessus et que je ne
l’applique à des clusters entiers.
Deux choses n’allaient pas, en revanche, et la première est sérieuse.
[inet_http_server]
port = 0.0.0.0:9001L’interface XML-RPC de Supervisor, exposée sur toutes les interfaces, sans
nom d’utilisateur ni mot de passe. Quiconque atteignait ce port pouvait arrêter,
démarrer ou relire les journaux de n’importe quel programme supervisé — le tout
tournant sous root. Sur un réseau domestique de 2020, le risque restait
théorique ; écrire cela aujourd’hui dans un dépôt me vaudrait un refus en revue,
et à juste titre.
La seconde est plus modeste : le script vérifie que le binaire supervisord
existe et que le service tourne, mais sa boucle principale s’exécute quand même
si ces contrôles échouent. La vérification était écrite, son résultat n’était
pas utilisé.
Enseignements
- Une constante de capteur est une relation entre deux unités : l’écrire en toutes lettres en commentaire aurait suffi à faire voir l’erreur de facteur 60.
- Un processus vivant n’est pas un processus qui fonctionne. Ici tout était au vert et le fichier de mesures restait vide.
- Une fonction d’interruption ne fait qu’une chose : incrémenter. Le calcul et l’écriture appartiennent à la boucle principale.
- Une erreur avalée par une bibliothèque est pire qu’un plantage : le plantage aurait signalé le défaut en deux secondes.
- Une interface d’administration sans authentification sur 0.0.0.0 reste une interface d’administration sans authentification, même sur un réseau domestique.
Ce projet n’a jamais produit une seule mesure exploitable, et c’est pour cela
qu’il a sa place ici. La partie réputée difficile — l’électronique,
l’interruption matérielle, la supervision — fonctionnait. Ce qui a échoué tenait
dans une division et dans deux %s de trop, sans que rien ne vienne le dire.
Six ans plus tard, l’essentiel de mon travail consiste à faire en sorte qu’une
situation pareille se signale d’elle-même.