Caso práctico: pluviómetro MQTT con Raspberry Pi 5

Objetivo y caso de uso

Qué construirás: Construirás una estación de monitoreo meteorológico resiliente y con capacidad offline que registra eventos de precipitación, les asigna marcas de tiempo precisas sin acceso a internet, los guarda en una base de datos local y transmite los datos a un broker MQTT.

Por qué es importante / Casos de uso

  • Agricultura de precisión: Monitorea la lluvia exacta en campos remotos para optimizar los horarios de riego, reduciendo el desperdicio de agua hasta en un 30% y previniendo la pudrición de los cultivos.
  • Sistemas de alerta temprana de inundaciones: Garantiza 0% de pérdida de datos durante cortes de red mediante el almacenamiento local en SQLite, entregando alertas MQTT en tiempo real con una latencia inferior a un segundo una vez que se restaure la conectividad.
  • Automatización del hogar inteligente: Integra los feeds MQTT en Home Assistant para retraer toldos o cerrar tragaluces automáticamente en <500ms tras la detección inicial de lluvia.
  • Investigación meteorológica: Despliega registradores autónomos en microclimas con Wi-Fi intermitente, dependiendo de un RTC DS3231 (precisión de ±2ppm, desviación de ~1 min/año) para una estricta integridad cronológica.

Resultado esperado

  • Un daemon ligero basado en Python ejecutándose continuamente con un consumo mínimo de recursos (<5% de CPU, <20MB de RAM).
  • Una base de datos SQLite local y robusta que almacena de forma segura todos los eventos de precipitación con marca de tiempo sin conexión.
  • Un publicador MQTT que se reconecta automáticamente y vacía los datos históricos en cola al broker tras la restauración de la red.

Audiencia: Desarrolladores de IoT, makers e ingenieros agrícolas; Nivel: Intermedio

Arquitectura/flujo: Sensor de lluvia → Interrupción GPIO → Daemon de Python → Marca de tiempo del RTC DS3231 → BD SQLite local → Broker MQTT → Dashboard del suscriptor

Nota educativa de validación

Antes de publicar este caso, el contenido pasó la puerta automática de validación de Prometeo con estado PASS. El validador comprobó los bloques de código, la estructura del artículo, los comandos copiables y la coherencia con el catálogo de dispositivos soportados.

Evidencia de validación publicada

  • Resultado automático: PASS.
  • Estructura parseada: 3 apartados, 3 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 2 Python/py_compile.
  • Catálogo soportado: el texto se contrastó contra los perfiles de dispositivo validables de Prometeo y los stacks no soportados bloquean la publicación.
  • Hallazgos del informe: sin hallazgos bloqueantes.

Esta validación confirma compatibilidad sintáctica y de herramientas para el material publicado, pero no sustituye la prueba física sobre tu hardware, cableado y entorno exactos.

Nota educativa de seguridad

Este proyecto es un prototipo educativo de bajo voltaje, no un producto certificado. Antes de encender la configuración, verifique la disposición de pines (pinout) de su Raspberry Pi exacta, nunca conecte 5 V a pines GPIO de 3.3 V, desconecte la alimentación antes de cambiar el cableado y use interfaces o fuentes externas adecuadas para sensores, relés, motores o cargas.

Diagrama de bloques conceptual

Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema.

Arquitectura funcional

Sensor de lluvia

Interrupción GPIO

Daemon de Python

Marca de tiempo del RTC DS3231

BD SQLite local

Broker MQTT

Dashboard del suscriptor

Flujo conceptual de señales y responsabilidades entre bloques del dispositivo.

Requisitos previos

  • Sistema operativo: Raspberry Pi OS Bookworm de 64 bits instalado y actualizado.
  • Entorno de software: Python 3.11 con soporte para entornos virtuales (python3-venv).
  • Red: Acceso a un broker MQTT local o basado en la nube (ej., Mosquitto).
  • Configuración del sistema: I2C habilitado a través de raspi-config para el RTC.

Materiales

  • Modelo del dispositivo: Raspberry Pi 5 + pluviómetro de balancín + RTC DS3231 + registro local SQLite
  • Conectividad: Cables puente hembra a hembra (jumper) para las conexiones GPIO.
  • Red: Conexión Wi-Fi o Ethernet para acceder al broker MQTT.

(Nota: La mayoría de los pluviómetros de balancín actúan como simples interruptores magnéticos de lengüeta (reed switches). Tienen dos cables y no tienen polaridad. Cada «vuelco» del balancín interno cierra momentáneamente el interruptor, lo que representa un volumen específico de lluvia, generalmente 0.2794 mm o 0.1 mm dependiendo del modelo).

Configuración/Conexión

La configuración física implica conectar el reloj de tiempo real (RTC) DS3231 a través del bus I2C y el pluviómetro de balancín a un pin GPIO estándar. La Raspberry Pi 5 cuenta con resistencias pull-up internas, que habilitaremos por software, simplificando el cableado para el pluviómetro.

Tabla de cableado

ComponentePin / Cable del componentePin de la Raspberry Pi 5Nombre del pin de la Pi 5Descripción
DS3231 RTCVCCPin 13.3V PowerAlimenta el módulo RTC
DS3231 RTCGNDPin 6GroundTierra común
DS3231 RTCSDAPin 3GPIO 2 (SDA)Línea de datos I2C
DS3231 RTCSCLPin 5GPIO 3 (SCL)Línea de reloj I2C
PluviómetroCable 1 (Sin polaridad)Pin 11GPIO 17Pin de interrupción para el vuelco del balancín
PluviómetroCable 2 (Sin polaridad)Pin 14GroundCompleta el circuito al cerrarse el interruptor

Configuración del RTC a nivel del sistema operativo

Para asegurarse de que la Raspberry Pi 5 utilice el DS3231 para su hora del sistema (crítico para el registro offline), debe configurar el árbol de dispositivos (device tree):
1. Abra la configuración de arranque: sudo nano /boot/firmware/config.txt
2. Agregue la siguiente línea al final: dtoverlay=i2c-rtc,ds3231
3. Guarde, salga y reinicie la Pi.
4. Verifique que el RTC sea detectado ejecutando i2cdetect -y 1. Debería ver UU en la dirección 0x68, lo que indica que el controlador del kernel ha reclamado el dispositivo.

Código validado

La siguiente implementación se divide en dos archivos. El primero es el registrador principal que maneja las interrupciones de hardware, el almacenamiento local de SQLite y la publicación MQTT. El segundo es un suscriptor MQTT ligero para validar el flujo de datos.

Ambos scripts admiten un modo de prueba (dry-run) a través de la variable de entorno MOCK_HARDWARE=1, lo que permite la ejecución y validación en cualquier computadora estándar.

Archivo 1: rain_logger.py

Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.

#!/usr/bin/env python3
"""
Objective: rain-gauge-mqtt-logger
Device: Raspberry Pi 5 + tipping bucket rain gauge + DS3231 RTC + local SQLite log
"""

import os
import time
import json
import sqlite3
import queue
import threading
from datetime import datetime, timezone
import paho.mqtt.client as mqtt

DB_FILE = "rain_log.db"
MQTT_BROKER = "localhost"  # Change to your broker IP if external
MQTT_PORT = 1883
MQTT_TOPIC = "weather/rain_gauge"
MM_PER_TIP = 0.2794  # Calibration metric: mm of rain per bucket tip
RAIN_PIN = 17

# Hardware Abstraction / Mocking
MOCK_MODE = os.environ.get("MOCK_HARDWARE", "0") == "1"

if MOCK_MODE:
    print("[INFO] Running in MOCK mode. Hardware GPIO bypassed.")
    class MockButton:
        def __init__(self, pin, pull_up, bounce_time):
            self.pin = pin
            self.when_pressed = None
            self._running = True
            self._thread = threading.Thread(target=self._simulate_rain, daemon=True)
            self._thread.start()

        def _simulate_rain(self):
            # Simulate a rain tip every 5 seconds
            while self._running:
                time.sleep(5)
                if self.when_pressed:
                    self.when_pressed()

    Button = MockButton
else:
    from gpiozero import Button

# Global Queue for thread-safe interrupt handling
tip_queue = queue.Queue()

def setup_database():
    """Initializes the SQLite database and creates the schema if it doesn't exist."""
    conn = sqlite3.connect(DB_FILE)
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS rain_events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            timestamp TEXT NOT NULL,
            daily_tips INTEGER NOT NULL,
            daily_mm REAL NOT NULL
        )
    ''')
    conn.commit()
    return conn

def bucket_tipped():
    """Hardware interrupt callback. Kept extremely short."""
    # Put a timestamp into the queue. The OS time is synced via the DS3231 RTC.
    tip_time = datetime.now(timezone.utc).isoformat()
    tip_queue.put(tip_time)
# ...

🔒 Este contenido es premium. Con la suscripción mensual (7 días gratis) podrás desbloquear el material didáctico completo y el pack PDF listo para imprimir.🔓 Desbloquéalo — 7 días gratis
#!/usr/bin/env python3
"""
Objective: rain-gauge-mqtt-logger
Device: Raspberry Pi 5 + tipping bucket rain gauge + DS3231 RTC + local SQLite log
"""

import os
import time
import json
import sqlite3
import queue
import threading
from datetime import datetime, timezone
import paho.mqtt.client as mqtt

DB_FILE = "rain_log.db"
MQTT_BROKER = "localhost"  # Change to your broker IP if external
MQTT_PORT = 1883
MQTT_TOPIC = "weather/rain_gauge"
MM_PER_TIP = 0.2794  # Calibration metric: mm of rain per bucket tip
RAIN_PIN = 17

# Hardware Abstraction / Mocking
MOCK_MODE = os.environ.get("MOCK_HARDWARE", "0") == "1"

if MOCK_MODE:
    print("[INFO] Running in MOCK mode. Hardware GPIO bypassed.")
    class MockButton:
        def __init__(self, pin, pull_up, bounce_time):
            self.pin = pin
            self.when_pressed = None
            self._running = True
            self._thread = threading.Thread(target=self._simulate_rain, daemon=True)
            self._thread.start()

        def _simulate_rain(self):
            # Simulate a rain tip every 5 seconds
            while self._running:
                time.sleep(5)
                if self.when_pressed:
                    self.when_pressed()

    Button = MockButton
else:
    from gpiozero import Button

# Global Queue for thread-safe interrupt handling
tip_queue = queue.Queue()

def setup_database():
    """Initializes the SQLite database and creates the schema if it doesn't exist."""
    conn = sqlite3.connect(DB_FILE)
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS rain_events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            timestamp TEXT NOT NULL,
            daily_tips INTEGER NOT NULL,
            daily_mm REAL NOT NULL
        )
    ''')
    conn.commit()
    return conn

def bucket_tipped():
    """Hardware interrupt callback. Kept extremely short."""
    # Put a timestamp into the queue. The OS time is synced via the DS3231 RTC.
    tip_time = datetime.now(timezone.utc).isoformat()
    tip_queue.put(tip_time)

def publish_mqtt(client, payload):
    """Publishes the JSON payload to the MQTT broker."""
    try:
        client.publish(MQTT_TOPIC, json.dumps(payload), qos=1)
        print(f"[MQTT] Published: {payload}")
    except Exception as e:
        print(f"[MQTT ERROR] Failed to publish: {e}")

def main():
    print("[INFO] Starting Rain Gauge MQTT Logger...")

    # Initialize Database
    db_conn = setup_database()
    db_cursor = db_conn.cursor()

    # Initialize MQTT Client
    mqtt_client = mqtt.Client()
    try:
        mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
        mqtt_client.loop_start()
        print(f"[INFO] Connected to MQTT Broker at {MQTT_BROKER}:{MQTT_PORT}")
    except Exception as e:
        print(f"[WARN] Could not connect to MQTT broker: {e}. Running in local-only mode.")

    # Initialize Hardware Interrupt
    # bounce_time handles the mechanical bouncing of the reed switch
    rain_gauge = Button(RAIN_PIN, pull_up=True, bounce_time=0.1)
    rain_gauge.when_pressed = bucket_tipped

    daily_tips = 0
    current_date = datetime.now(timezone.utc).date()

    print("[INFO] System ready. Waiting for rain events...")

    try:
        while True:
            try:
                # Block until an event occurs (timeout allows clean exit on KeyboardInterrupt)
                tip_time_str = tip_queue.get(timeout=1.0)

                # Check for day rollover to reset daily counters
                event_date = datetime.fromisoformat(tip_time_str).date()
                if event_date > current_date:
                    daily_tips = 0
                    current_date = event_date

                daily_tips += 1
                daily_mm = round(daily_tips * MM_PER_TIP, 4)

                # 1. Save to local SQLite database
                db_cursor.execute(
                    "INSERT INTO rain_events (timestamp, daily_tips, daily_mm) VALUES (?, ?, ?)",
                    (tip_time_str, daily_tips, daily_mm)
                )
                db_conn.commit()
                print(f"[DB] Logged tip {daily_tips} at {tip_time_str} ({daily_mm} mm total)")

                # 2. Publish to MQTT
                payload = {
                    "timestamp": tip_time_str,
                    "event": "bucket_tip",
                    "daily_tips": daily_tips,
                    "daily_mm": daily_mm
                }
                publish_mqtt(mqtt_client, payload)

            except queue.Empty:
                continue

    except KeyboardInterrupt:
        print("\n[INFO] Shutting down logger...")
    finally:
        db_conn.close()
        mqtt_client.loop_stop()
        mqtt_client.disconnect()

if __name__ == "__main__":
    main()

Archivo 2: mqtt_subscriber_test.py

#!/usr/bin/env python3
"""
Utility script to validate MQTT broadcasts from the rain logger.
"""

import paho.mqtt.client as mqtt
import json

MQTT_BROKER = "localhost"
MQTT_PORT = 1883
MQTT_TOPIC = "weather/rain_gauge"

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print(f"[INFO] Connected to broker. Subscribing to {MQTT_TOPIC}...")
        client.subscribe(MQTT_TOPIC)
    else:
        print(f"[ERROR] Connection failed with code {rc}")

def on_message(client, userdata, msg):
    try:
        payload = json.loads(msg.payload.decode())
        print(f"\n--- New Rain Event Received ---")
        print(f"Time : {payload.get('timestamp')}")
        print(f"Tips : {payload.get('daily_tips')}")
        print(f"Total: {payload.get('daily_mm')} mm")
    except json.JSONDecodeError:
        print(f"[WARN] Received non-JSON message: {msg.payload}")

def main():
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message

    print("[INFO] Starting MQTT Test Subscriber...")
    client.connect(MQTT_BROKER, MQTT_PORT, 60)

    try:
        client.loop_forever()
    except KeyboardInterrupt:
        print("\n[INFO] Disconnecting...")
        client.disconnect()

if __name__ == "__main__":
    main()

Comandos de compilación/flasheo/ejecución

Use los siguientes comandos para configurar el entorno, instalar dependencias y ejecutar el sistema.

Tabla de comandos

TareaComandoDescripción
Dependencias del sistemasudo apt update && sudo apt install -y mosquitto mosquitto-clients python3-venv sqlite3Instala el broker MQTT local, las herramientas venv de Python y la CLI de SQLite.
Crear entornopython3 -m venv ~/rain_envCrea un entorno de Python aislado para las dependencias.
Activar entornosource ~/rain_env/bin/activateActiva el entorno virtual.
Instalar paquetespip install paho-mqtt==1.6.1 gpiozeroInstala las bibliotecas de Python requeridas (versión específica de MQTT para estabilidad de la API).
Ejecutar registrador (Simulado)MOCK_HARDWARE=1 python3 rain_logger.pyEjecuta la aplicación principal en modo de omisión de hardware.
Ejecutar suscriptorpython3 mqtt_subscriber_test.pyEjecuta el cliente de prueba para ver los mensajes entrantes.

Flujo de trabajo de ejecución

  1. Abra un terminal en su Raspberry Pi 5 (o PC para el modo simulado).
  2. Ejecute el comando de dependencias del sistema para asegurarse de que mosquitto se esté ejecutando localmente.
  3. Cree y active el entorno virtual de Python.
  4. Instale paho-mqtt y gpiozero.
  5. Abra una segunda ventana de terminal, active el mismo entorno y ejecute python3 mqtt_subscriber_test.py.
  6. En el primer terminal, ejecute MOCK_HARDWARE=1 python3 rain_logger.py.
  7. Observe los vuelcos de lluvia simulados automatizados que aparecen en ambos terminales.

Validación paso a paso

Use estos puntos de control para confirmar que el prototipo funciona correctamente.

  1. Comprobación de sintaxis y ejecución simulada
  2. Acción: Ejecute python3 -m py_compile rain_logger.py y luego ejecute el registrador con MOCK_HARDWARE=1.
  3. Observación esperada: Sin errores de sintaxis. El terminal muestra [INFO] Running in MOCK mode y registra un vuelco simulado cada 5 segundos.
  4. Condición de aprobación: Las inserciones en la base de datos y las publicaciones MQTT aparecen en la salida de la consola sin rastreos de errores (tracebacks).

  5. Comprobación de persistencia local en SQLite

  6. Acción: Detenga el registrador (Ctrl+C). Ejecute sqlite3 rain_log.db "SELECT * FROM rain_events;".
  7. Observación esperada: La consola imprime filas de datos que contienen un ID, una marca de tiempo ISO 8601, el conteo de vuelcos y el valor en mm.
  8. Condición de aprobación: Los datos permanecen intactos y consultables después de que el script de Python haya terminado, demostrando la durabilidad offline.

  9. Comprobación de transmisión de red MQTT

  10. Acción: Ejecute el registrador en el terminal 1 y mqtt_subscriber_test.py en el terminal 2.
  11. Observación esperada: El terminal 2 muestra bloques formateados --- New Rain Event Received --- que coinciden con las marcas de tiempo generadas en el terminal 1.
  12. Condición de aprobación: El payload JSON se serializa correctamente, se transmite a través del broker local, se recibe y el suscriptor lo analiza.

  13. Comprobación de interrupción de hardware físico (Solo Pi 5 física)

  14. Acción: Ejecute python3 rain_logger.py (sin MOCK_HARDWARE=1). Vuelque manualmente el balancín del pluviómetro físico de un lado a otro.
  15. Observación esperada: Aparece una nueva entrada de registro inmediatamente tras cada vuelco físico del balancín.
  16. Condición de aprobación: El tiempo de rebote (bounce time) de gpiozero filtra con éxito el ruido mecánico del interruptor (sin conteos dobles para un solo vuelco).

  17. Comprobación de marca de tiempo offline del RTC (Solo Pi 5 física)

  18. Acción: Desconecte la Pi 5 de la red. Reiníciela. Ejecute el registrador, vuelque el balancín y revise la base de datos.
  19. Observación esperada: La marca de tiempo en SQLite refleja con precisión la hora actual del mundo real, a pesar de no tener acceso a un servidor NTP.
  20. Condición de aprobación: El tiempo es estrictamente monótono y preciso, demostrando que la integración del DS3231 por I2C funciona a nivel del sistema operativo.

Solución de problemas

SíntomaCausa probableSolución
Múltiples vuelcos contados por un solo vuelco físicoEl rebote del interruptor supera el umbral de eliminación de rebotes del software.Aumente bounce_time=0.1 a 0.2 o 0.3 en la inicialización de Button.
ConnectionRefusedError en MQTTEl broker Mosquitto no se está ejecutando o no está instalado.Ejecute sudo systemctl start mosquitto o instálelo vía apt.
El script falla con ModuleNotFoundErrorEl entorno virtual no está activado.Ejecute source ~/rain_env/bin/activate antes de ejecutar el script.
La hora del RTC es completamente incorrecta sin conexióndtoverlay no está cargado o la batería del RTC está agotada.Revise dmesg \| grep rtc. Reemplace la batería CR2032 en el módulo DS3231.
No se registran eventos cuando el balancín se vuelcaCableado defectuoso o pin GPIO incorrecto.Verifique que el pluviómetro esté conectado al Pin 11 (GPIO 17) y a Tierra (Pin 14). Pruebe la continuidad del pluviómetro con un multímetro.

Mejoras

Hardware y durabilidad
* Impermeabilización: Encierre la Raspberry Pi 5, el DS3231 y todas las conexiones en una carcasa resistente a la intemperie con clasificación IP67. Use prensaestopas para los cables del pluviómetro para evitar la entrada de humedad.
* Resiliencia de energía: Agregue un HAT de Sistema de Alimentación Ininterrumpida (UPS) a la Pi 5 para asegurar que el registro continúe durante cortes de energía inducidos por tormentas.

Datos y analítica
* Depuración de la base de datos: Implemente un trabajo cron semanal o un hilo en segundo plano para exportar registros antiguos de SQLite a CSV y purgar la base de datos para evitar el crecimiento ilimitado del archivo en la tarjeta SD.
* Payloads MQTT avanzados: Expanda el payload JSON para incluir promedios móviles (ej., tasa de lluvia en mm/h) para proporcionar más contexto al dashboard receptor.

Confiabilidad del sistema
* Servicio Systemd: Envuelva rain_logger.py en un archivo de servicio systemd estándar para que se inicie automáticamente en el arranque y se reinicie en caso de falla.
* Temporizador Watchdog: Habilite el watchdog de hardware de la Raspberry Pi para reiniciar forzosamente el sistema de forma automática si el sistema operativo se bloquea durante un evento climático extremo.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el objetivo principal del proyecto descrito en el artículo?




Pregunta 2: ¿Qué tecnología de base de datos se utiliza para almacenar los datos localmente durante cortes de red?




Pregunta 3: En el caso de uso de agricultura de precisión, ¿cuál es uno de los beneficios directos de monitorear la lluvia exacta?




Pregunta 4: ¿Qué protocolo se utiliza para transmitir los datos de la estación una vez que hay conectividad?




Pregunta 5: ¿Cuánto tiempo tarda el sistema en retraer toldos o cerrar tragaluces tras la detección inicial de lluvia en un hogar inteligente?




Pregunta 6: ¿Qué componente de hardware garantiza la estricta integridad cronológica sin acceso a internet?




Pregunta 7: ¿Cuál es la desviación de tiempo aproximada del reloj en tiempo real (RTC) mencionado en el texto?




Pregunta 8: ¿En qué lenguaje de programación está basado el daemon ligero del sistema?




Pregunta 9: ¿Cuál es el consumo de recursos esperado para el daemon que se ejecuta continuamente?




Pregunta 10: ¿Qué sucede con los datos históricos en cola cuando se restaura la conexión de red?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio