Raspberry Pi

Sensor de distancia MQTT con Raspberry Pi: HC-SR04 para nivel de tanques y detección de presencia

Conecta un sensor ultrasónico HC-SR04 a una Raspberry Pi con un divisor de tensión seguro, filtra las lecturas ruidosas en Python y publica el nivel de llenado de un tanque y eventos de presencia por MQTT para dashboards y automatizaciones.

Sensor ultrasónico HC-SR04 montado sobre un tanque de agua y conectado a una Raspberry Pi que envía lecturas de nivel, un proyecto de sensor de distancia MQTT con Raspberry Pi
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

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:

  1. EMPTY_CM: del sensor al fondo (o al nivel útil más bajo).
  2. 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:

  1. Control de bombas: un segundo script o un flujo de Node-RED se suscribe a level y 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.
  2. Alertas: en un broker privado de ConnectMQ, una regla de alerta por umbral sobre level_pct te avisa de un tanque casi vacío, como se describe en alertas por umbral MQTT.
  3. Domótica: Home Assistant puede leer el topic retenido presence como 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.

Artículos relacionados

Fiabilidad de la Raspberry Pi: cómo usar una Pi 24/7 como gateway IoT

Una guía práctica para mantener un gateway MQTT con Raspberry Pi funcionando durante meses: desgaste del almacenamiento, sistemas de archivos de solo lectura, alimentación, refrigeración, watchdogs de hardware y software y monitorización remota de la salud.

9 min de lectura

Pico W vs Pico 2 W para MQTT: rendimiento, memoria y TLS de RP2040 frente a RP2350

La Pico 2 W mantiene el formato y el chip Wi-Fi de la Pico W, pero sustituye el RP2040 por el RP2350, más rápido. Esta comparativa analiza qué significa para los dispositivos MQTT: CPU, memoria, TLS, funciones de seguridad, soporte de software y qué placa comprar para cada proyecto.

9 min de lectura

Registro de accesos Raspberry Pi RFID MQTT con el lector RC522

Conecta un lector RFID RC522 a una Raspberry Pi, lee etiquetas en Python, compáralas con una lista blanca local y publica cada intento de acceso como un evento MQTT que pueden consumir registradores, dashboards y herramientas de alertas.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.