Un projet de capteur de distance Raspberry Pi MQTT avec le HC-SR04 résout deux problèmes courants avec un seul composant bon marché : à quel point une cuve est-elle pleine, et y a-t-il quelque chose (ou quelqu'un) devant le capteur. Dans ce tutoriel, vous câblerez le capteur à ultrasons en toute sécurité, obtiendrez des mesures stables en Python avec gpiozero, et publierez en MQTT à la fois un pourcentage de remplissage et des événements de présence afin qu'un tableau de bord ou une automatisation puisse agir.
Comment le HC-SR04 mesure la distance
Le HC-SR04 émet une brève salve d'ultrasons à 40 kHz lorsque sa broche TRIG reçoit une impulsion de 10 microsecondes, puis maintient sa broche ECHO à l'état haut jusqu'au retour de l'écho. La durée de cette impulsion d'écho, multipliée par la vitesse du son et divisée par deux, donne la distance. Le capteur fonctionne de quelques centimètres à quelques mètres, avec de meilleurs résultats face à des surfaces planes et dures perpendiculaires, comme une surface d'eau ou un mur.
Deux conséquences comptent pour de vrais projets. D'abord, le Pi doit chronométrer l'impulsion d'écho de manière logicielle, si bien que l'ordonnancement de Linux ajoute un peu de gigue ; vous la filtrerez. Ensuite, la vitesse du son varie avec la température de l'air : attendez-vous à une légère dérive entre une matinée froide et un après-midi chaud. Pour un suivi de niveau de cuve ou un détecteur de présence, ces deux effets restent gérables.
Câblage avec un pont diviseur de tension
Le HC-SR04 fonctionne en 5 V et sa sortie ECHO est aussi en 5 V, ce qui peut endommager une broche GPIO du Raspberry Pi. Utilisez toujours un pont diviseur de tension sur ECHO :
| Broche HC-SR04 | Connexion |
|---|---|
| VCC | 5 V du Pi (broche physique 2) |
| GND | GND du Pi (broche physique 6) |
| TRIG | GPIO23 (broche physique 16), en direct |
| ECHO | Résistance de 1 k vers GPIO24 (broche physique 18) ; résistance de 2 k de GPIO24 vers GND |
La paire 1 k et 2 k ramène les 5 V à environ 3,3 V. Une combinaison 1 k et 1,8 k ou 2,2 k fonctionne également. TRIG peut être relié directement, car la sortie 3,3 V du Pi suffit à déclencher le capteur. Si vous achetez la variante du module compatible 3,3 V, consultez sa documentation ; le diviseur n'est peut-être pas nécessaire.
Installer les logiciels
sudo apt update
sudo apt install -y python3-venv python3-lgpio
mkdir -p ~/distance-mqtt && cd ~/distance-mqtt
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
pip install "paho-mqtt>=2.0" gpiozero
Test rapide sans MQTT :
python -c "from gpiozero import DistanceSensor; from time import sleep; s = DistanceSensor(echo=24, trigger=23, max_distance=4); sleep(1); print(round(s.distance * 100, 1), 'cm')"
Pointez le capteur vers un mur et comparez le résultat avec un mètre ruban. Une mesure toujours égale à la valeur max_distance signifie qu'aucun écho n'est détecté, généralement à cause d'un problème de câblage sur ECHO.
Deux modes : niveau et présence
Une même mesure peut être interprétée de deux façons, et le script publie les deux :
| Mode | Calcul | Topic | Publication | Retenu |
|---|---|---|---|---|
| Niveau de cuve | (empty_distance - distance) / (empty_distance - full_distance) |
.../level |
Toutes les 30 s | Oui |
| Présence | Distance sous un seuil, avec hystérésis | .../presence |
Uniquement en cas de changement | Oui |
| Brut | Distance médiane en cm | dans les deux payloads |
Retenir les deux topics garantit qu'un tableau de bord ouvert à tout moment affiche immédiatement le niveau actuel et la présence ou non de quelqu'un. Ne publier la présence qu'en cas de changement réduit le trafic au minimum et simplifie les automatisations : il suffit de s'abonner et de réagir à chaque message.
Calibrer la cuve
Montez le capteur en haut de la cuve, pointé droit vers le bas, à l'écart de l'arrivée d'eau et des parois. Mesurez ensuite deux distances :
EMPTY_CM: du capteur au fond (ou au niveau utile le plus bas).FULL_CM: du capteur à l'eau au repère de remplissage maximal. Laissez au moins la portée minimale du capteur, quelques centimètres, entre sa face et le niveau d'eau le plus haut.
Définissez ces deux valeurs dans l'environnement. Le script borne le résultat entre 0 et 100 % afin que les éclaboussures ou une cuve vide ne produisent pas de valeurs impossibles.
Le script du capteur de distance Raspberry Pi MQTT
Enregistrez-le sous distance_mqtt.py :
#!/usr/bin/env python3
"""HC-SR04 tank level and presence detection over MQTT."""
import json
import os
import statistics
import time
import uuid
import paho.mqtt.client as mqtt
from gpiozero import DistanceSensor
BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"distance-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/9c3d55/tank1")
EMPTY_CM = float(os.getenv("EMPTY_CM", "120"))
FULL_CM = float(os.getenv("FULL_CM", "20"))
PRESENCE_ON_CM = float(os.getenv("PRESENCE_ON_CM", "60"))
PRESENCE_OFF_CM = float(os.getenv("PRESENCE_OFF_CM", "80"))
LEVEL_INTERVAL = 30
SAMPLES = 7
sensor = DistanceSensor(echo=24, trigger=23, max_distance=4, queue_len=5)
def measure_cm():
"""Median of several readings, ignoring out-of-range values."""
values = []
for _ in range(SAMPLES):
d = sensor.distance * 100
if 2 < d < 399: # 400 cm signifie aucun écho dans max_distance
values.append(d)
time.sleep(0.06) # laisser les échos s'éteindre entre deux impulsions
return statistics.median(values) if values else None
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Connection refused: {reason_code}")
return
print(f"Connected as {CLIENT_ID}")
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.connect(BROKER, PORT, keepalive=30)
client.loop_start()
present = None
last_level = 0.0
try:
while True:
distance = measure_cm()
now = time.time()
if distance is None:
print("No valid echo")
time.sleep(1)
continue
# Présence avec hystérésis : activée sous ON, désactivée au-dessus de OFF.
new_present = present
if distance < PRESENCE_ON_CM:
new_present = True
elif distance > PRESENCE_OFF_CM:
new_present = False
if new_present is not None and new_present != present:
present = new_present
payload = {"present": present, "distance_cm": round(distance, 1), "ts": int(now)}
client.publish(f"{PREFIX}/presence", json.dumps(payload), qos=1, retain=True)
print("presence", payload)
if now - last_level >= LEVEL_INTERVAL:
span = EMPTY_CM - FULL_CM
percent = max(0.0, min(100.0, (EMPTY_CM - distance) / span * 100))
payload = {"level_pct": round(percent, 1), "distance_cm": round(distance, 1), "ts": int(now)}
client.publish(f"{PREFIX}/level", json.dumps(payload), qos=1, retain=True)
print("level", payload)
last_level = now
time.sleep(0.5)
except KeyboardInterrupt:
pass
finally:
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(2)
client.loop_stop()
client.disconnect()
sensor.close()
Dans un vrai déploiement, vous utiliseriez généralement un seul mode par capteur : un capteur de cuve n'a pas besoin d'événements de présence, et un capteur de passage de porte n'a pas besoin de taux de remplissage. Garder les deux dans un même script facilite l'expérimentation, et vous pouvez supprimer le bloc inutile.
Pourquoi le filtrage par médiane et l'hystérésis comptent
Les mesures à ultrasons font parfois des sauts : un écho parasite sur une paroi de la cuve, une ondulation à la surface de l'eau ou un hoquet de timing sous Linux. Une moyenne laisse une seule valeur aberrante fausser le résultat ; la médiane de sept mesures l'ignore tout simplement. L'hystérésis résout un autre problème. Sans elle, une personne immobile pile au niveau du seuil ferait basculer la présence plusieurs fois par seconde, inondant les abonnés d'événements. Avec des distances d'activation et de désactivation distinctes, l'état change une fois et reste stable.
Tester la sortie
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-9c3d55 \
-t 'connectmq/demo/9c3d55/tank1/#' -v
Approchez la main du capteur : un message presence avec "present": true apparaît quand vous passez sous 60 cm, et false dès que vous vous éloignez au-delà de 80 cm. Toutes les 30 secondes, un message level indique le pourcentage de remplissage selon votre calibration. N'oubliez pas que le serveur MQTT public gratuit est lisible par tout le monde : utilisez un préfixe aléatoire et n'indiquez pas de vrais noms de sites dans les topics de test.
Automatiser à partir des données
Avec le niveau et la présence sur MQTT, l'automatisation devient une affaire d'abonnés plutôt que de capteur :
- Commande de pompe : un second script ou un flow Node-RED s'abonne à
levelet active une pompe via le contrôleur relais MQTT sur Raspberry Pi quand le niveau passe sous 20 %, puis la coupe au-dessus de 90 %. - Alertes : sur un broker ConnectMQ privé, une règle d'alerte de seuil sur
level_pctvous prévient d'une cuve presque vide, comme décrit dans alertes de seuil MQTT. - Domotique : dans une maison connectée, Home Assistant peut lire le topic retenu
presencecomme capteur d'occupation et allumer les lumières ; Jeedom et Domoticz savent eux aussi communiquer en MQTT.
Si vous avez besoin d'un capteur de niveau sur batterie pour une cuve éloignée sans alimentation secteur, le montage capteur de niveau de cuve d'eau ESP32 utilise le même payload avec le deep sleep, si bien que les deux peuvent alimenter le même tableau de bord.
Dépannage
| Problème | Cause probable et solution |
|---|---|
| Mesure toujours 400 cm | Aucun écho détecté ; vérifiez le diviseur et le câblage d'ECHO |
| Les mesures sautent fortement | Le capteur voit la paroi de la cuve ; montez-le au centre, loin des bords |
| Les mesures dérivent dans la journée | La vitesse du son varie avec la température ; recalibrez ou ajoutez une compensation de température |
| Erreurs dues à la condensation dans une cuve | Humidité sur les transducteurs ; utilisez un module à ultrasons étanche |
GPIO busy |
Un autre script utilise GPIO23 ou GPIO24 |
Mise en production
Pour une cuve dans une ferme, un atelier ou un bâtiment, passez du broker de test à un broker privé. ConnectMQ est un broker MQTT managé avec TLS sur le port 8883 et des identifiants générés. Définissez dans l'environnement MQTT_PORT=8883, MQTT_USERNAME=YOUR_USERNAME, MQTT_PASSWORD=YOUR_PASSWORD, MQTT_CLIENT_ID=YOUR_CLIENT_ID et un préfixe explicite comme farm1/water/tank1, puis exécutez le script comme service systemd. Le tutoriel MQTT sur Raspberry Pi fournit le modèle de fichier de service et explique le comportement de reconnexion utilisé ici.
Démarrer avec ConnectMQ
Placez vos données de niveau et de présence sur un broker MQTT cloud accessible à tous vos tableaux de bord et automatisations. Inscrivez-vous sur ConnectMQ avec l'offre Serverless gratuite, sans carte bancaire, créez un broker et connectez votre Pi en TLS en quelques minutes.
Questions fréquentes
Faut-il vraiment un pont diviseur de tension pour le HC-SR04 ?
Oui, pour le module standard en 5 V. Sa broche ECHO délivre 5 V, alors que les broches GPIO du Raspberry Pi ne supportent que 3,3 V.
Quelle est la précision du HC-SR04 sur un Raspberry Pi ?
De l'ordre du centimètre dans de bonnes conditions, une fois les mesures filtrées par médiane. Le timing logiciel sous Linux et les variations de température ajoutent un peu de variabilité.
Le HC-SR04 peut-il mesurer le niveau d'eau dans une cuve fermée ?
Oui, tant qu'il pointe droit vers la surface et reste au sec. Dans les cuves humides, un module à ultrasons étanche dure plus longtemps.
Pourquoi ne publier la présence qu'en cas de changement ?
Cela réduit le trafic au minimum et simplifie les automatisations. Comme le message est retenu, les nouveaux abonnés reçoivent tout de même immédiatement l'état actuel.



