Caso práctico: monitor de sal con Raspberry Pi 5

Raspberry Pi 5 connected to a small sensor board and a 2.13-inch OLED display via a ribbon cable; a beaker of water and a pile of salt sit in the background, illustrating a salt-monitor project.

Objetivo y caso de uso

Qué construirás: Construirás un monitor inteligente de sal para descalcificador de agua que utiliza un sensor láser de tiempo de vuelo (ToF) I2C para medir la distancia física a la pila de sal. Calcula el porcentaje de sal restante y muestra el estado en una pantalla de tinta electrónica (e-paper) SPI de consumo ultrabajo.

Por qué es importante / Casos de uso

  • Previene daños por agua dura: Evita fallos en la regeneración, impidiendo que el agua dura incruste cal en las tuberías y destruya electrodomésticos como calentadores de agua y lavavajillas.
  • Reduce las revisiones manuales: Elimina la necesidad de levantar las pesadas tapas de los depósitos de salmuera en sótanos oscuros al proporcionar una lectura externa de alto contraste y siempre activa.
  • Mantenimiento proactivo: Rastrea las tasas de agotamiento de la sal para que los propietarios puedan programar la compra de pesados sacos de sal sin viajes de emergencia a la ferretería.
  • Integración de hardware: Demuestra cómo conectar un sensor I2C de bajo ancho de banda (típicamente 400kHz) para la adquisición de datos con un periférico SPI de mayor ancho de banda (hasta 20MHz) para la renderización de píxeles.

Resultado esperado

  • Un monitor de nivel de sal completamente funcional que ofrece mediciones de distancia ToF con precisión milimétrica y una latencia del sensor <30ms.
  • Un panel de tinta electrónica siempre activo que retiene su imagen con un consumo de energía activa de 0W entre actualizaciones.
  • Un bucle de firmware robusto que muestrea datos, actualiza la pantalla (típicamente 2-3s de tiempo de actualización) y maximiza la duración de la batería.

Audiencia: Entusiastas del hogar inteligente (DIY), administradores de instalaciones y estudiantes de hardware; Nivel: Intermedio

Arquitectura/flujo: El MCU se despierta → Consulta el sensor ToF a través de I2C → Calcula el porcentaje de sal a partir de la profundidad del depósito → Genera un cuadro de interfaz de usuario (UI) → Envía el búfer a la pantalla de tinta electrónica a través de SPI → Entra en suspensión profunda (deep sleep).

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, 2 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 1 Python/py_compile, 1 Bash/copy-paste checks.
  • 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 el esquema de pines (pinout) de su Raspberry Pi exacta, nunca conecte 5 V a los pines GPIO de 3.3 V, desconecte la alimentación antes de cambiar el cableado y use interfaces adecuadas o fuentes externas 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

El MCU se despierta

Consulta el sensor ToF a través de I2C

Calcula el porcentaje de sal a partir de…

Genera un cuadro de interfaz de usuario (UI)

Envía el búfer a la pantalla de tinta ele…

Entra en suspensión profunda (deep sleep)

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

Requisitos previos

Antes de ensamblar el hardware y ejecutar el código, asegúrese de tener listo lo siguiente:
* Una Raspberry Pi 5 con Raspberry Pi OS Bookworm de 64 bits.
* Python 3.11 o superior instalado (python3 --version).
* Las interfaces I2C y SPI habilitadas en la Raspberry Pi. Puede habilitarlas ejecutando sudo raspi-config, navegando a Interface Options y habilitando tanto I2C como SPI.
* Familiaridad básica con la línea de comandos de Linux y la navegación por directorios.
* Una fuente de alimentación USB-C de 5V/5A para la Raspberry Pi 5.

Materiales

Debe utilizar los componentes exactos enumerados a continuación para garantizar la compatibilidad con el cableado y el código proporcionados:
* Controlador: Raspberry Pi 5 (modelo de 4GB u 8GB)
* Sensor: Sensor de distancia ToF VL53L0X (placa de conexión estándar con pines I2C)
* Pantalla: Pantalla de tinta electrónica SPI de 2.13 pulgadas (estilo Waveshare, resolución de 250×122, blanco/negro)
* Cableado: Cables puente hembra a hembra (al menos 12 cables)
* Montaje: Cinta de doble cara o un soporte impreso en 3D para montar el sensor orientado hacia abajo en el interior de la tapa del depósito de salmuera.

Configuración/Conexión

El proyecto utiliza dos buses de comunicación diferentes. El sensor VL53L0X utiliza el bus I2C, que requiere solo dos cables de datos (SDA y SCL) más alimentación. La pantalla de tinta electrónica de 2.13 pulgadas utiliza el bus SPI, que requiere una línea de reloj, líneas de datos y varios pines de control para la selección de chip (chip select), conmutación de datos/comandos, reinicio (reset) y estado de ocupado (busy).

Asegúrese de que su Raspberry Pi 5 esté completamente apagada y desenchufada antes de realizar cualquier conexión.

Tabla de configuración de pines (Pinout)

ComponentePin del componentePin físico RPi 5Nombre / Función GPIO RPi 5
VL53L0XVCC / VINPin 1Alimentación 3.3V
VL53L0XGNDPin 9Tierra (GND)
VL53L0XSDAPin 3GPIO 2 (I2C1 SDA)
VL53L0XSCLPin 5GPIO 3 (I2C1 SCL)
E-PaperVCCPin 17Alimentación 3.3V
E-PaperGNDPin 20Tierra (GND)
E-PaperDIN / MOSIPin 19GPIO 10 (SPI0 MOSI)
E-PaperCLK / SCLKPin 23GPIO 11 (SPI0 SCLK)
E-PaperCSPin 24GPIO 8 (SPI0 CE0)
E-PaperDCPin 22GPIO 25
E-PaperRSTPin 11GPIO 17
E-PaperBUSYPin 18GPIO 24

Nota: El sensor VL53L0X debe montarse en la parte inferior de la tapa del descalcificador de agua, apuntando directamente hacia abajo a la sal. Asegúrese de que el camino esté libre de tubos o mecanismos internos.

Código validado

El siguiente script de Python (salt_monitor.py) contiene la lógica completa para el monitor de nivel de sal del descalcificador de agua. Cuenta con clases de adaptadores de hardware que recurren elegantemente a implementaciones simuladas (mock) si falta el hardware físico o si se pasa la bandera --dry-run. Esto garantiza que el código sea válido para py_compile y comprobable en cualquier máquina.

salt_monitor.py

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

#!/usr/bin/env python3
"""
Water Softener Salt Level Monitor
Target: Raspberry Pi 5 + VL53L0X ToF Sensor + 2.13 inch SPI e-paper display
"""

import argparse
import time
import logging
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# --- Calibration Constants ---
# Distance from the sensor to the salt when the tank is completely full
TANK_FULL_DISTANCE_MM = 150  
# Distance from the sensor to the bottom of the tank (or minimum salt level)
TANK_EMPTY_DISTANCE_MM = 850 

class VL53L0XAdapter:
    """Adapter for the VL53L0X Time-of-Flight sensor."""
    def __init__(self, dry_run=False):
        self.dry_run = dry_run
        self.sensor = None
        self.mock_distance = 200 # Starting mock distance in mm

        if not self.dry_run:
            try:
                import board
                import busio
                import adafruit_vl53l0x
                i2c = busio.I2C(board.SCL, board.SDA)
                self.sensor = adafruit_vl53l0x.VL53L0X(i2c)
                logging.info("Hardware VL53L0X initialized successfully.")
            except ImportError:
                logging.warning("adafruit_vl53l0x not found. Falling back to Mock VL53L0X.")
                self.dry_run = True
            except Exception as e:
                logging.error(f"Failed to initialize hardware VL53L0X: {e}. Falling back to Mock.")
                self.dry_run = True

    def get_distance(self):
        """Returns distance in millimeters."""
        if self.dry_run:
            # Simulate salt depletion over time for demonstration purposes
            self.mock_distance += 15 
            if self.mock_distance > TANK_EMPTY_DISTANCE_MM + 50:
                self.mock_distance = TANK_FULL_DISTANCE_MM
            return self.mock_distance
        else:
            try:
                return self.sensor.range
            except Exception as e:
                logging.error(f"Error reading from sensor: {e}")
                return -1

class EPaperAdapter:
    """Adapter for the 2.13 inch SPI e-paper display."""
    def __init__(self, dry_run=False):
        self.dry_run = dry_run
        self.epd = None

        if not self.dry_run:
            try:
                # Attempting to load standard Waveshare library structure
                from waveshare_epd import epd2in13_V4
                self.epd = epd2in13_V4.EPD()
                self.epd.init()
                self.epd.Clear(0xFF)
                logging.info("Hardware E-Paper initialized successfully.")
            except ImportError:
                logging.warning("waveshare_epd library not found. Falling back to Mock E-Paper.")
                self.dry_run = True
            except Exception as e:
                logging.error(f"Failed to init hardware E-Paper: {e}. Falling back to Mock.")
                self.dry_run = True
# ...

🔒 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
"""
Water Softener Salt Level Monitor
Target: Raspberry Pi 5 + VL53L0X ToF Sensor + 2.13 inch SPI e-paper display
"""

import argparse
import time
import logging
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# --- Calibration Constants ---
# Distance from the sensor to the salt when the tank is completely full
TANK_FULL_DISTANCE_MM = 150  
# Distance from the sensor to the bottom of the tank (or minimum salt level)
TANK_EMPTY_DISTANCE_MM = 850 

class VL53L0XAdapter:
    """Adapter for the VL53L0X Time-of-Flight sensor."""
    def __init__(self, dry_run=False):
        self.dry_run = dry_run
        self.sensor = None
        self.mock_distance = 200 # Starting mock distance in mm

        if not self.dry_run:
            try:
                import board
                import busio
                import adafruit_vl53l0x
                i2c = busio.I2C(board.SCL, board.SDA)
                self.sensor = adafruit_vl53l0x.VL53L0X(i2c)
                logging.info("Hardware VL53L0X initialized successfully.")
            except ImportError:
                logging.warning("adafruit_vl53l0x not found. Falling back to Mock VL53L0X.")
                self.dry_run = True
            except Exception as e:
                logging.error(f"Failed to initialize hardware VL53L0X: {e}. Falling back to Mock.")
                self.dry_run = True

    def get_distance(self):
        """Returns distance in millimeters."""
        if self.dry_run:
            # Simulate salt depletion over time for demonstration purposes
            self.mock_distance += 15 
            if self.mock_distance > TANK_EMPTY_DISTANCE_MM + 50:
                self.mock_distance = TANK_FULL_DISTANCE_MM
            return self.mock_distance
        else:
            try:
                return self.sensor.range
            except Exception as e:
                logging.error(f"Error reading from sensor: {e}")
                return -1

class EPaperAdapter:
    """Adapter for the 2.13 inch SPI e-paper display."""
    def __init__(self, dry_run=False):
        self.dry_run = dry_run
        self.epd = None

        if not self.dry_run:
            try:
                # Attempting to load standard Waveshare library structure
                from waveshare_epd import epd2in13_V4
                self.epd = epd2in13_V4.EPD()
                self.epd.init()
                self.epd.Clear(0xFF)
                logging.info("Hardware E-Paper initialized successfully.")
            except ImportError:
                logging.warning("waveshare_epd library not found. Falling back to Mock E-Paper.")
                self.dry_run = True
            except Exception as e:
                logging.error(f"Failed to init hardware E-Paper: {e}. Falling back to Mock.")
                self.dry_run = True

    def update_display(self, percentage, distance):
        """Updates the e-paper display with current salt level."""
        timestamp = datetime.now().strftime('%H:%M')

        # Create a visual bar representation
        bar_length = 20
        filled_blocks = max(0, min(bar_length, int((percentage / 100.0) * bar_length)))
        empty_blocks = bar_length - filled_blocks
        visual_bar = f"[{'#' * filled_blocks}{'-' * empty_blocks}]"

        status_text = "OK"
        if percentage < 20:
            status_text = "REFILL SOON!"
        if percentage <= 5:
            status_text = "EMPTY! REFILL NOW!"

        display_string = (
            f"=== SALT LEVEL ===\n"
            f"Level: {percentage:.1f}%\n"
            f"Dist:  {distance} mm\n"
            f"{visual_bar}\n"
            f"Status: {status_text}\n"
            f"Updated: {timestamp}\n"
            f"=================="
        )

        if self.dry_run:
            logging.info(f"Mock E-Paper Update:\n{display_string}")
        else:
            try:
                # In a real implementation, we would use PIL (Pillow) to draw text onto an image buffer
                # and send it to self.epd.display(). For this validated pure-Python representation,
                # we log the buffer action that would occur.
                from PIL import Image, ImageDraw
                # Create blank image
                image = Image.new('1', (self.epd.height, self.epd.width), 255)
                draw = ImageDraw.Draw(image)
                # Draw text (using default font for simplicity)
                draw.text((10, 10), display_string, fill=0)
                # Rotate image if necessary for screen orientation
                image = image.rotate(90, expand=True)
                self.epd.display(self.epd.getbuffer(image))
                logging.info("Hardware display updated.")
            except Exception as e:
                logging.error(f"Failed to write to hardware display: {e}")

    def sleep(self):
        """Puts the e-paper display to sleep to prevent damage."""
        if not self.dry_run and self.epd:
            try:
                self.epd.sleep()
            except Exception as e:
                logging.error(f"Failed to sleep hardware display: {e}")

def calculate_percentage(distance_mm):
    """Calculates the remaining salt percentage based on calibration distances."""
    if distance_mm <= TANK_FULL_DISTANCE_MM:
        return 100.0
    if distance_mm >= TANK_EMPTY_DISTANCE_MM:
        return 0.0

    usable_range = TANK_EMPTY_DISTANCE_MM - TANK_FULL_DISTANCE_MM
    current_depletion = distance_mm - TANK_FULL_DISTANCE_MM
    percentage = 100.0 - ((current_depletion / usable_range) * 100.0)
    return max(0.0, min(100.0, percentage))

def main():
    parser = argparse.ArgumentParser(description="Water Softener Salt Level Monitor")
    parser.add_argument("--dry-run", action="store_true", help="Run without physical hardware")
    parser.add_argument("--interval", type=int, default=10, help="Polling interval in seconds")
    args = parser.parse_args()

    logging.info("Starting Water Softener Salt Level Monitor...")
    if args.dry_run:
        logging.info("Running in DRY-RUN mode.")

    sensor = VL53L0XAdapter(dry_run=args.dry_run)
    display = EPaperAdapter(dry_run=args.dry_run)

    last_percentage = -100.0 # Force initial update

    try:
        while True:
            distance = sensor.get_distance()
            if distance < 0:
                logging.warning("Invalid sensor reading. Retrying next cycle.")
                time.sleep(args.interval)
                continue

            percentage = calculate_percentage(distance)
            logging.info(f"Reading: {distance}mm -> {percentage:.1f}%")

            # Update display only if percentage changes by more than 2% to save screen lifespan
            if abs(percentage - last_percentage) >= 2.0:
                logging.info("Significant change detected. Updating display...")
                display.update_display(percentage, distance)
                last_percentage = percentage
                # Put display back to sleep after update
                display.sleep()

            time.sleep(args.interval)

    except KeyboardInterrupt:
        logging.info("Monitor stopped by user.")
    finally:
        display.sleep()
        logging.info("Shutdown complete.")

if __name__ == "__main__":
    main()

setup_and_run.sh

Este script crea un entorno de Python aislado, instala las dependencias necesarias y ejecuta el monitor.

#!/bin/bash
# Setup and execution script for Salt Monitor

echo "1. Creating Python virtual environment..."
python3 -m venv salt_env

echo "2. Activating virtual environment..."
source salt_env/bin/activate

echo "3. Installing dependencies..."
# adafruit-circuitpython-vl53l0x provides the I2C driver
# RPi.GPIO and spidev are typically required by Waveshare libraries
# Pillow is required for drawing text on the e-paper buffer
pip install adafruit-circuitpython-vl53l0x RPi.GPIO spidev Pillow

echo "4. Running the monitor in dry-run mode for validation..."
python3 salt_monitor.py --dry-run --interval 2

Quiz rápido

Pregunta 1: ¿Qué mide el sensor VL53L0X en este monitor de sal?




Pregunta 2: ¿Por qué se calibra una distancia de depósito lleno y otra de depósito vacío?




Pregunta 3: ¿Qué ventaja tiene una pantalla e-paper en este tipo de montaje?




Pregunta 4: ¿Qué parte debe mantenerse compatible con 3,3 V en la Raspberry Pi?




Pregunta 5: ¿Qué evita la lógica de umbral con histéresis o margen?




Pregunta 6: ¿Para qué sirve el modo de validación sin hardware real?




Pregunta 7: ¿Qué dato conviene registrar junto al porcentaje de sal?




Pregunta 8: ¿Qué indica un porcentaje bajo en este caso práctico?




Pregunta 9: ¿Qué limitación educativa debe recordar el alumno?




Pregunta 10: ¿Qué comunicación usa normalmente el VL53L0X con la Raspberry Pi?




Comandos de Compilación/Flasheo/Ejecución

Utilice el siguiente flujo de trabajo de comandos compactos para que su proyecto funcione en la Raspberry Pi 5.

ComandoAcción
python3 -m py_compile salt_monitor.pyValida la sintaxis de Python sin ejecutarlo.
chmod +x setup_and_run.shHace que el script de configuración sea ejecutable.
./setup_and_run.shCrea el entorno, instala dependencias y ejecuta una prueba simulada.
source salt_env/bin/activateActiva el entorno virtual para ejecuciones manuales.
python3 salt_monitor.py --interval 3600Ejecuta el bucle de hardware real (se actualiza cada 1 hora).

Flujo de trabajo:
1. Guarde el código Python como salt_monitor.py en el directorio de su proyecto.
2. Guarde el script bash como setup_and_run.sh en el mismo directorio.
3. Ejecute los comandos de la tabla anterior para validar, instalar y ejecutar el monitor.

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.

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