Un proyecto de sensor de distancia MQTT con Raspberry Pi y el HC-SR04 resuelve dos problemas habituales con una sola pieza barata: qué tan lleno está un tanque y si hay algo (o alguien) delante. En este tutorial conectarás el sensor ultrasónico de forma segura, obtendrás lecturas estables en Python con gpiozero y publicarás tanto un porcentaje de llenado como eventos de presencia por MQTT para que un dashboard o una automatización puedan actuar.
Cómo mide la distancia el HC-SR04
El HC-SR04 emite una breve ráfaga de ultrasonido a 40 kHz cuando su pin TRIG recibe un pulso de 10 microsegundos y después mantiene su pin ECHO en alto hasta que vuelve el reflejo. La duración de ese pulso de eco, multiplicada por la velocidad del sonido y dividida entre dos, es la distancia. El sensor funciona desde un par de centímetros hasta unos pocos metros, con mejores resultados frente a superficies planas y duras en ángulo recto, como la superficie del agua o una pared.
Hay dos consecuencias que importan en proyectos reales. Primero, la Pi tiene que cronometrar el pulso de eco por software, así que la planificación de Linux añade algo de jitter; lo filtrarás. Segundo, la velocidad del sonido cambia con la temperatura del aire, así que espera una pequeña deriva entre una mañana fría y una tarde calurosa. Para un medidor de nivel de tanque o un detector de presencia, ambos efectos son manejables.
Cableado con divisor de tensión
El HC-SR04 funciona a 5 V y su salida ECHO también es de 5 V, lo que puede dañar un pin GPIO de la Raspberry Pi. Usa siempre un divisor de tensión en ECHO:
| Pin del HC-SR04 | Conexión |
|---|---|
| VCC | 5 V de la Pi (pin físico 2) |
| GND | GND de la Pi (pin físico 6) |
| TRIG | GPIO23 (pin físico 16), directo |
| ECHO | Resistencia de 1 k a GPIO24 (pin físico 18); resistencia de 2 k de GPIO24 a GND |
La pareja de 1 k y 2 k divide los 5 V hasta unos 3,3 V. También funciona una combinación de 1 k con 1,8 k o 2,2 k. TRIG puede conectarse directamente porque la salida de 3,3 V de la Pi basta para disparar el sensor. Si compras la variante del módulo compatible con 3,3 V, consulta su documentación; puede que no necesites el divisor.
Instalar el software
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
Prueba rápida sin 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')"
Apunta el sensor a una pared y compara el resultado con una cinta métrica. Una lectura que siempre es exactamente el valor de max_distance significa que no se detecta eco, normalmente por un problema de cableado en ECHO.
Dos modos: nivel y presencia
La misma lectura puede interpretarse de dos maneras, y el script publica ambas:
| Modo | Cálculo | Topic | Cuándo se publica | Retenido |
|---|---|---|---|---|
| Nivel del tanque | (empty_distance - distance) / (empty_distance - full_distance) |
.../level |
Cada 30 s | Sí |
| Presencia | Distancia por debajo de un umbral, con histéresis | .../presence |
Solo al cambiar | Sí |
| Bruto | Distancia mediana en cm | dentro de ambos payloads |
Retener ambos topics significa que un dashboard abierto en cualquier momento muestra de inmediato el nivel actual y si hay alguien presente. Publicar la presencia solo cuando cambia mantiene el tráfico mínimo y simplifica las automatizaciones: basta con suscribirse y reaccionar a cada mensaje.
Calibrar el tanque
Monta el sensor en la parte superior del tanque, apuntando hacia abajo, lejos de la entrada de agua y de las paredes. Después mide dos distancias:
EMPTY_CM: del sensor al fondo (o al nivel útil más bajo).FULL_CM: del sensor al agua en la marca de lleno. Deja al menos el alcance mínimo del sensor, unos pocos centímetros, entre la cara del sensor y el nivel de agua más alto.
Define ambos valores en el entorno. El script limita el resultado entre 0 y 100 por ciento para que las salpicaduras o un tanque vacío no produzcan valores imposibles.
El script del sensor de distancia MQTT con Raspberry Pi
Guárdalo como 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():
"""Mediana de varias lecturas, ignorando los valores fuera de rango."""
values = []
for _ in range(SAMPLES):
d = sensor.distance * 100
if 2 < d < 399: # 400 cm significa que no hubo eco dentro de max_distance
values.append(d)
time.sleep(0.06) # dejar que se apaguen los ecos entre pulsos
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
# Presencia con histéresis: se activa por debajo de ON y se desactiva por encima 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()
En un despliegue real normalmente usarías un solo modo por sensor: un sensor de tanque no necesita eventos de presencia, y un sensor de puerta no necesita un nivel de llenado. Mantener ambos en un script facilita experimentar, y puedes borrar el bloque que no necesites.
Por qué importan el filtrado por mediana y la histéresis
Las lecturas ultrasónicas saltan de vez en cuando: un eco perdido en la pared del tanque, una onda en el agua o un tropiezo de temporización en Linux. Promediar deja que un valor erróneo arrastre el resultado; la mediana de siete lecturas simplemente lo ignora. La histéresis resuelve otro problema. Sin ella, una persona situada justo en el umbral activaría y desactivaría la presencia varias veces por segundo, inundando a los suscriptores de eventos. Con distancias de activación y desactivación separadas, el estado cambia una vez y se mantiene.
Probar la salida
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-9c3d55 \
-t 'connectmq/demo/9c3d55/tank1/#' -v
Acerca la mano al sensor: aparece un mensaje presence con "present": true al cruzar los 60 cm y false cuando te alejas más de 80 cm. Cada 30 segundos, un mensaje level informa del porcentaje de llenado según tu calibración. Recuerda que el servidor MQTT público gratuito es legible por cualquiera, así que usa un prefijo aleatorio y no pongas nombres reales de instalaciones en los topics de prueba.
Automatizar con los datos
Con el nivel y la presencia en MQTT, la automatización pasa a ser un problema del suscriptor y no del sensor:
- Control de bombas: un segundo script o un flujo de Node-RED se suscribe a
levely activa una bomba mediante el controlador de relé MQTT con Raspberry Pi cuando el nivel baja del 20 por ciento, y la apaga por encima del 90 por ciento. - Alertas: en un broker privado de ConnectMQ, una regla de alerta por umbral sobre
level_pctte avisa de un tanque casi vacío, como se describe en alertas por umbral MQTT. - Domótica: Home Assistant puede leer el topic retenido
presencecomo sensor de ocupación y encender las luces.
Si necesitas un sensor de nivel a batería en un tanque remoto sin red eléctrica, el montaje de sensor de nivel de tanque de agua con ESP32 usa el mismo payload con deep sleep, así que ambos pueden alimentar el mismo dashboard.
Solución de problemas
| Problema | Causa probable y solución |
|---|---|
| Siempre marca 400 cm | No se detecta eco; revisa el divisor y el cableado de ECHO |
| Las lecturas saltan sin control | El sensor ve la pared del tanque; móntalo en el centro y lejos de los bordes |
| Las lecturas derivan a lo largo del día | La velocidad del sonido cambia con la temperatura; recalibra o añade compensación de temperatura |
| Errores por condensación en un tanque | Humedad en los transductores; usa un módulo ultrasónico sumergible |
GPIO busy |
Otro script está usando GPIO23 o GPIO24 |
Configuración de producción
Para un tanque en una finca, un taller o un edificio, pasa del servidor de pruebas a uno privado. ConnectMQ es un broker MQTT gestionado con TLS en el puerto 8883 y credenciales generadas. Define en el entorno MQTT_PORT=8883, MQTT_USERNAME=YOUR_USERNAME, MQTT_PASSWORD=YOUR_PASSWORD, MQTT_CLIENT_ID=YOUR_CLIENT_ID y un prefijo significativo como farm1/water/tank1, y ejecuta el script como servicio systemd. El tutorial de MQTT con Raspberry Pi incluye la plantilla del archivo de servicio y explica el comportamiento de reconexión que se usa aquí.
Empieza con ConnectMQ
Lleva tus datos de nivel y presencia a un broker MQTT en la nube al que puedan llegar todos tus dashboards y automatizaciones. Regístrate en ConnectMQ en el plan Serverless gratuito, sin tarjeta de crédito, crea un broker y conecta tu Pi por TLS en minutos.
Preguntas frecuentes
¿De verdad necesito un divisor de tensión para el HC-SR04?
Sí, con el módulo estándar de 5 V. Su pin ECHO entrega 5 V, y los pines GPIO de la Raspberry Pi solo admiten 3,3 V.
¿Qué precisión tiene el HC-SR04 en una Raspberry Pi?
Alrededor de un centímetro en buenas condiciones, una vez filtradas las lecturas por mediana. La temporización por software en Linux y los cambios de temperatura añaden algo de variación.
¿Puede el HC-SR04 medir el nivel de agua en un tanque cerrado?
Sí, siempre que apunte directamente a la superficie y se mantenga seco. En tanques húmedos, un módulo ultrasónico sumergible dura más.
¿Por qué publicar la presencia solo cuando cambia?
Mantiene el tráfico al mínimo y simplifica las automatizaciones. Como el mensaje es retenido, los nuevos suscriptores siguen recibiendo el estado actual de inmediato.



