You dont have javascript enabled! Please enable it!

Caso práctico: Panel e‑Paper Raspberry Pi 5+Waveshare 2.9

Caso práctico: Panel e‑Paper Raspberry Pi 5+Waveshare 2.9 — hero

Objetivo y caso de uso

Qué construirás: Un panel de control en vivo que muestra métricas del sistema en una pantalla e-Paper de 2.9″ conectada a una Raspberry Pi 5 mediante SPI.

Para qué sirve

  • Monitoreo en tiempo real de la CPU, RAM y uso de disco.
  • Visualización de la temperatura del sistema para gestión térmica.
  • Control de la conectividad de red mediante métricas de tráfico.
  • Actualizaciones rápidas de datos gracias a la interfaz SPI.

Resultado esperado

  • Actualizaciones de métricas cada 5 segundos.
  • Latencia de respuesta en la visualización menor a 200 ms.
  • Consumo de CPU del script inferior al 5% durante la ejecución.
  • Visualización de datos con un refresh rate de 2 Hz en la pantalla e-Paper.

Público objetivo: Desarrolladores avanzados; Nivel: Avanzado

Arquitectura/flujo: Raspberry Pi 5 -> SPI -> Pantalla e-Paper 2.9″ -> Visualización de métricas del sistema.

Nivel: Avanzado

Prerrequisitos

Este caso práctico construye un “spi-epaper-live-dashboard” que muestra, en tiempo casi real, métricas del propio sistema (CPU, RAM, red, temperatura y disco) en una pantalla e‑Paper de 2.9″ conectada por SPI a una Raspberry Pi 5. Se apoya en una toolchain concreta y versiones fijadas para garantizar reproducibilidad.

Sistema operativo y toolchain exactos

  • Sistema operativo:
  • Raspberry Pi OS Bookworm 64‑bit
  • Kernel Linux rama 6.x (Bookworm)
  • Python 3.11 (stock en Bookworm)
  • Toolchain de usuario (versiones exactas a instalar en el entorno virtual):
  • pip 24.2
  • setuptools 72.1.0
  • wheel 0.44.0
  • spidev 3.6
  • lgpio 0.2.2.0
  • gpiozero 2.0
  • Pillow 10.4.0
  • psutil 5.9.8
  • requests 2.32.3

Tabla resumen de versiones y componentes:

Componente Versión/Valor Notas
Raspberry Pi OS Bookworm 64‑bit Imagen oficial para Raspberry Pi 5
Python 3.11.x Predeterminado en Bookworm
pip 24.2 Fijado en el venv
spidev 3.6 Acceso a /dev/spidevX.Y
lgpio 0.2.2.0 Backend gpiod para Pi 5
gpiozero 2.0 GPIO de alto nivel sobre lgpio
Pillow 10.4.0 Renderizado de gráficos en RAM
psutil 5.9.8 Métricas del sistema
requests 2.32.3 Opcional: datos remotos (e.g., un KPI REST)
Bus SPI SPI0 CE0 (/dev/spidev0.0) Línea de datos de la pantalla
Frecuencia SPI 4 MHz Estable y seguro para panel Waveshare

Requisitos de hardware y software previos:

  • Conexión a Internet para instalar paquetes.
  • Usuario con privilegios sudo.
  • Habilitación de SPI en el sistema.
  • Editor de texto (nano, vim).

Materiales

  • 1 × Raspberry Pi 5 (4 GB o 8 GB, cualquiera funciona para este proyecto).
  • 1 × MicroSD (32 GB recomendado) con Raspberry Pi OS Bookworm 64‑bit.
  • 1 × Fuente de alimentación oficial USB‑C 5V/5A para Raspberry Pi 5.
  • 1 × Pantalla “Waveshare 2.9″ e‑Paper HAT” (modelo monocromo 296×128, llamada también 2.9″ V2).
  • 1 × Conector/HAT de 40 pines (incluido en la Waveshare e‑Paper HAT).
  • Acceso a red (Ethernet o Wi‑Fi) para instalar dependencias.
  • Opcional: disipador o ventilador para la Raspberry Pi 5 si va a operar de forma continua.

Importante: Este tutorial está diseñado específicamente para “Raspberry Pi 5 + Waveshare 2.9″ e‑Paper HAT” y el proyecto “spi-epaper-live-dashboard”. Todos los pasos, código y comandos se han adaptado para este modelo.

Preparación y conexión

Actualización del sistema e instalación base

1) Actualiza el sistema y herramientas base:
– sudo apt update
– sudo apt full-upgrade -y
– sudo apt install -y git python3.11 python3.11-venv python3-pip gpiod

2) Reinicia:
– sudo reboot

Habilitar SPI

Puedes habilitar SPI con raspi-config o editando el archivo de arranque.

Opción A: raspi-config (TUI)
– sudo raspi-config
– Interface Options → SPI → Enable
– Finish → Reboot

Opción B: edición directa de /boot/firmware/config.txt
– sudo nano /boot/firmware/config.txt
– Asegura que exista la línea: dtparam=spi=on
– Guarda y reinicia: sudo reboot

Verifica el dispositivo SPI:
– ls -l /dev/spidev0.0

Deberías ver /dev/spidev0.0. Si no aparece, revisa “Troubleshooting”.

Conexionado del HAT (pines)

La Waveshare 2.9″ e‑Paper HAT está diseñada para acoplarse directamente al conector de 40 pines. Si la pinchas como un HAT, no necesitas cableado adicional. No obstante, si utilizas cables sueltos o quieres validar la asignación, esta es la correspondencia más común para la 2.9″ HAT monocroma en Raspberry Pi:

Señal e‑Paper HAT Pin Raspberry Pi Nombre físico GPIO (BCM) Descripción
VCC 1 o 17 3V3 Alimentación lógica 3.3 V
GND 6, 9, 14, etc. GND Tierra
DIN 19 MOSI GPIO10 Datos SPI
CLK 23 SCLK GPIO11 Reloj SPI
CS 24 CE0 GPIO8 Chip Select SPI0
DC 22 GPIO25 GPIO25 Data/Command
RST 11 GPIO17 GPIO17 Reset del panel
BUSY 18 GPIO24 GPIO24 Señal de ocupado (busy)

Notas:
– Usaremos /dev/spidev0.0 (bus 0, CE0).
– El BUSY de muchos controladores de e‑Paper activo en bajo indica “ocupado”. El código lo tendrá en cuenta.
– Asegúrate de alinear correctamente el HAT en el conector; la muesca y la serigrafía de pin 1 deben coincidir.

Código completo

A continuación se presentan dos archivos:
– epaper29.py: un driver mínimo para la Waveshare 2.9″ e‑Paper HAT (monocromo) usando SPI (spidev) y GPIO (gpiozero sobre lgpio).
– dashboard.py: la aplicación “spi-epaper-live-dashboard” que dibuja métricas del sistema con Pillow y actualiza la e‑Paper a intervalos.

Antes de ejecutar, crearás un entorno virtual y fijarás versiones exactas en la sección de compilación/ejecución.

epaper29.py (driver mínimo del panel 2.9″)

Este driver implementa inicialización, borrado, visualización y suspensión. Está orientado al panel monocromo 296×128 (Waveshare 2.9″ V2), con controlador UC8151/SSD1680. Se usa actualización completa para simplificar, estable y sin ghosting en dashboards.

# epaper29.py
# Driver mínimo para Waveshare 2.9" e-Paper HAT (monocromo, 296x128) en Raspberry Pi 5
# SPI: /dev/spidev0.0  | GPIO: DC=25, RST=17, BUSY=24
# Toolchain: spidev==3.6, gpiozero==2.0 (pin factory lgpio), Pillow==10.4.0

import time
import spidev
from gpiozero import DigitalOutputDevice, DigitalInputDevice
from PIL import Image

class EPaper29:
    # Dimensiones nativas del controlador (ancho x alto)
    WIDTH = 128
    HEIGHT = 296

    # Comandos del controlador (UC8151/SSD1680/SSD1681 common)
    PANEL_SETTING           = 0x00
    POWER_SETTING           = 0x01
    POWER_OFF               = 0x02
    POWER_ON                = 0x04
    BOOSTER_SOFT_START      = 0x06
    DATA_START_TRANSMISSION_1 = 0x10
    DATA_START_TRANSMISSION_2 = 0x13
    DISPLAY_REFRESH         = 0x12  # Algunos controladores usan 0x20 con 0x22 set; en UC8151 0x12 es válido
    VCOM_AND_DATA_INTERVAL  = 0x50
    TCON_RESOLUTION         = 0x61
    VCM_DC_SETTING_REGISTER = 0x82
    PARTIAL_WINDOW          = 0x90
    DEEP_SLEEP              = 0x07
    DATA_STOP               = 0x11

    def __init__(self, spi_bus=0, spi_device=0, spi_hz=4000000,
                 pin_dc=25, pin_rst=17, pin_busy=24, spi_mode=0):
        # GPIO
        self.dc = DigitalOutputDevice(pin_dc, active_high=True, initial_value=False)
        self.rst = DigitalOutputDevice(pin_rst, active_high=True, initial_value=True)
        self.busy = DigitalInputDevice(pin_busy, pull_up=True)
        # SPI
        self.spi = spidev.SpiDev()
        self.spi.open(spi_bus, spi_device)
        self.spi.max_speed_hz = spi_hz
        self.spi.mode = spi_mode

        # Track orientation
        self.rotate_180 = True  # la HAT suele mapear más cómodo con rotación

    def _send_command(self, cmd):
        self.dc.off()
        self.spi.writebytes([cmd])

    def _send_data(self, data):
        self.dc.on()
        if isinstance(data, int):
            self.spi.writebytes([data])
        else:
            # data es una secuencia/bytes
            self.spi.writebytes(list(data))

    def _reset(self):
        # Reset por hardware
        self.rst.on()
        time.sleep(0.01)
        self.rst.off()
        time.sleep(0.01)
        self.rst.on()
        time.sleep(0.05)

    def _wait_until_idle(self, timeout=5.0):
        start = time.time()
        # BUSY = 0 => ocupado; 1 => listo (en muchos controladores de esta familia)
        while not self.busy.value:
            if (time.time() - start) > timeout:
                # time-out de seguridad
                break
            time.sleep(0.01)

    def init(self):
        # Secuencia de init validada para 2.9" B/W V2 (UC8151/SSD1680)
        self._reset()

        # POWER ON
        self._send_command(self.POWER_ON)
        self._wait_until_idle(timeout=5.0)

        # PANEL SETTING
        # 0xAF: KW-BF=1, KWR=1, LUT from OTP, B/W mode
        self._send_command(self.PANEL_SETTING)
        self._send_data(0xAF)

        # VCOM AND DATA INTERVAL
        # 0xF0: default (reduce ghosting)
        self._send_command(self.VCOM_AND_DATA_INTERVAL)
        self._send_data(0xF0)

        # TCON RESOLUTION (Width, Height)
        self._send_command(self.TCON_RESOLUTION)
        self._send_data(self.WIDTH & 0xFF)         # 0x80 (128)
        self._send_data((self.HEIGHT >> 8) & 0xFF) # 0x01
        self._send_data(self.HEIGHT & 0xFF)        # 0x28 (296)

        # VCM DC SETTING
        self._send_command(self.VCM_DC_SETTING_REGISTER)
        self._send_data(0x12)

        self._wait_until_idle(timeout=1.0)

    def clear(self, color=1):
        # color=1 (blanco), 0 (negro)
        # Escritura monocapa al RAM de imagen
        fill_byte = 0xFF if color else 0x00
        pixels = (self.WIDTH * self.HEIGHT) // 8
        buf = bytes([fill_byte] * pixels)

        self._send_command(self.DATA_START_TRANSMISSION_1)
        self._send_data(buf)
        self._send_command(self.DATA_STOP)

        self._update()

    def _update(self):
        # DISPLAY REFRESH
        self._send_command(self.DISPLAY_REFRESH)
        self._wait_until_idle(timeout=10.0)

    def display_image(self, image: Image.Image):
        # Acepta PIL Image en modo '1' o 'L' y la empaqueta a bits (1bpp)
        img = image.convert('1')
        if self.rotate_180:
            img = img.rotate(180, expand=True)

        # Ajusta tamaño exacto del framebuffer
        if img.size != (self.WIDTH, self.HEIGHT):
            img = img.resize((self.WIDTH, self.HEIGHT))

        # Empaquetar 8 pixeles por byte. Convención: 1=blanco, 0=negro
        pixels = img.load()
        packed = bytearray()
        for y in range(self.HEIGHT):
            byte = 0
            bit_count = 0
            for x in range(self.WIDTH):
                pixel = pixels[x, y]
                bit = 1 if pixel == 255 else 0
                byte = (byte << 1) | bit
                bit_count += 1
                if bit_count == 8:
                    packed.append(byte & 0xFF)
                    byte = 0
                    bit_count = 0
            if bit_count != 0:
                # relleno si WIDTH no múltiplo de 8 (no aplica, 128 es múltiplo de 8)
                byte <<= (8 - bit_count)
                packed.append(byte & 0xFF)

        # Transmisión de imagen
        self._send_command(self.DATA_START_TRANSMISSION_1)
        self._send_data(packed)
        self._send_command(self.DATA_STOP)

        # Refrescar
        self._update()

    def sleep(self):
        # POWER OFF + DEEP SLEEP
        self._send_command(self.POWER_OFF)
        self._wait_until_idle(timeout=2.0)
        self._send_command(self.DEEP_SLEEP)
        self._send_data(0xA5)

    def close(self):
        try:
            self.spi.close()
        except Exception:
            pass

Puntos clave del driver:
– Usa gpiozero con la factoría lgpio (verás cómo la establecemos con una variable de entorno al ejecutar).
– Controla DC, RST y BUSY por GPIO; el BUSY se lee en polling al refrescar.
– Escribe el framebuffer en modo 1 bit/pixel con convención 1=blanco, 0=negro.
– Fuerza rotación 180° para que el texto sea natural con el HAT en la orientación típica; puedes desactivarlo si tu montaje es distinto.

dashboard.py (aplicación “spi-epaper-live-dashboard”)

La aplicación recolecta datos del propio sistema (psutil) y los dibuja con Pillow. El layout es de alta legibilidad monocroma. Actualiza cada minuto con refresco completo para evitar ghosting en sesiones largas.

# dashboard.py
# "spi-epaper-live-dashboard" en Raspberry Pi 5 + Waveshare 2.9" e-Paper HAT
# Toolchain: Pillow==10.4.0, psutil==5.9.8, requests==2.32.3 (opcional), spidev==3.6, gpiozero==2.0
import os
import time
import socket
import psutil
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont

from epaper29 import EPaper29

# Fuentes del sistema (Bookworm): DejaVu Sans Mono es una opción segura
FONT_MONO = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
FONT_SANS = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"

def get_ip_address():
    try:
        hostname = socket.gethostname()
        ip = socket.gethostbyname(hostname)
        # Si devuelve 127.0.0.1, intenta otra vía
        if ip.startswith("127."):
            # Prueba con una conexión UDP dummy para resolver interfaz principal
            import socket as s
            sckt = s.socket(s.AF_INET, s.SOCK_DGRAM)
            sckt.connect(("8.8.8.8", 80))
            ip = sckt.getsockname()[0]
            sckt.close()
        return ip
    except Exception:
        return "0.0.0.0"

def gather_metrics():
    cpu_pct = psutil.cpu_percent(interval=0.5)
    load1, load5, load15 = psutil.getloadavg()
    mem = psutil.virtual_memory()
    disk = psutil.disk_usage("/")
    temp_c = None
    # Temperatura de CPU (VCGENCMD) o psutil.sensors_temperatures
    try:
        temps = psutil.sensors_temperatures()
        if "cpu_thermal" in temps and temps["cpu_thermal"]:
            temp_c = temps["cpu_thermal"][0].current
        elif "coretemp" in temps and temps["coretemp"]:
            temp_c = temps["coretemp"][0].current
    except Exception:
        pass
    # Si no obtuvimos temp, intenta vcgencmd
    if temp_c is None:
        try:
            import subprocess
            out = subprocess.check_output(["vcgencmd", "measure_temp"], text=True).strip()
            # Ej: temp=50.0'C
            if "=" in out and "'C" in out:
                temp_c = float(out.split("=")[1].split("'C")[0])
        except Exception:
            temp_c = 0.0

    net_bytes = psutil.net_io_counters()
    ip = get_ip_address()

    now = datetime.now()
    return {
        "time": now.strftime("%Y-%m-%d %H:%M"),
        "cpu_pct": cpu_pct,
        "load1": load1,
        "load5": load5,
        "load15": load15,
        "mem_used": mem.used,
        "mem_total": mem.total,
        "mem_pct": mem.percent,
        "disk_used": disk.used,
        "disk_total": disk.total,
        "disk_pct": disk.percent,
        "temp_c": temp_c,
        "ip": ip,
        "bytes_sent": net_bytes.bytes_sent,
        "bytes_recv": net_bytes.bytes_recv,
    }

def human_bytes(n):
    # Conversión amigable
    step = 1024.0
    for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
        if n < step:
            return f"{n:3.1f}{unit}"
        n /= step
    return f"{n:.1f}PiB"

def draw_dashboard(metrics, width=128, height=296):
    # Crea una imagen monocroma 1bpp para la e-Paper
    img = Image.new("1", (width, height), 1)  # 1=blanco
    draw = ImageDraw.Draw(img)

    # Fuentes (tamaños ajustados a 296x128 vertical)
    font_title = ImageFont.truetype(FONT_SANS, 16)
    font_mono_small = ImageFont.truetype(FONT_MONO, 12)
    font_mono_tiny = ImageFont.truetype(FONT_MONO, 10)

    # Márgenes
    x0, y0 = 4, 4
    line = 16

    # Encabezado
    draw.text((x0, y0), "SPI e-Paper Live Dashboard", font=font_title, fill=0)
    y = y0 + line + 6

    # Hora e IP
    draw.text((x0, y), f"{metrics['time']}  IP:{metrics['ip']}", font=font_mono_small, fill=0)
    y += line

    # CPU y carga
    draw.text((x0, y), f"CPU: {metrics['cpu_pct']:>5.1f}%  T:{metrics['temp_c']:>4.1f}C", font=font_mono_small, fill=0)
    y += line
    draw.text((x0, y), f"Load: {metrics['load1']:.2f} {metrics['load5']:.2f} {metrics['load15']:.2f}", font=font_mono_small, fill=0)
    y += line

    # Memoria
    mem_used = human_bytes(metrics["mem_used"])
    mem_total = human_bytes(metrics["mem_total"])
    draw.text((x0, y), f"RAM: {mem_used}/{mem_total} ({metrics['mem_pct']:>5.1f}%)", font=font_mono_small, fill=0)
    y += line

    # Disco
    disk_used = human_bytes(metrics["disk_used"])
    disk_total = human_bytes(metrics["disk_total"])
    draw.text((x0, y), f"Disk: {disk_used}/{disk_total} ({metrics['disk_pct']:>5.1f}%)", font=font_mono_small, fill=0)
    y += line

    # Red
    draw.text((x0, y), f"Net: Tx {human_bytes(metrics['bytes_sent'])}", font=font_mono_small, fill=0)
    y += line
    draw.text((x0, y), f"     Rx {human_bytes(metrics['bytes_recv'])}", font=font_mono_small, fill=0)
    y += line

    # Footer
    y_footer = height - 18
    draw.line([(x0, y_footer-4), (width-4, y_footer-4)], fill=0, width=1)
    draw.text((x0, y_footer), "Raspberry Pi 5 + Waveshare 2.9\" e-Paper HAT", font=font_mono_tiny, fill=0)

    return img

def main():
    # Usa gpiozero sobre lgpio en Raspberry Pi 5 (Bookworm)
    # Exporta antes de ejecutar: GPIOZERO_PIN_FACTORY=lgpio
    epd = EPaper29(spi_bus=0, spi_device=0, spi_hz=4_000_000, pin_dc=25, pin_rst=17, pin_busy=24)

    try:
        epd.init()
        epd.clear(color=1)  # Blanco

        # bucle principal: refresco completo cada 60s
        refresh_sec = 60
        while True:
            m = gather_metrics()
            img = draw_dashboard(m, width=EPaper29.WIDTH, height=EPaper29.HEIGHT)
            epd.display_image(img)
            # Cada minuto, suficiente para la dinámica del sistema sin exprimir el panel
            time.sleep(refresh_sec)

    except KeyboardInterrupt:
        pass
    finally:
        try:
            epd.sleep()
        except Exception:
            pass
        epd.close()

if __name__ == "__main__":
    # Asegura la ruta de fuentes; si no existen, utiliza una fuente por defecto
    if not os.path.exists(FONT_MONO):
        # fallback simple a PIL default (menos estético)
        pass
    main()

Breve explicación de las partes clave:
– gather_metrics usa psutil para obtener CPU, carga, RAM, disco, red y temperatura. Incluye una ruta alternativa con vcgencmd si psutil no expone sensores.
– draw_dashboard monta un layout monocromo para 296×128, con tipografías del sistema DejaVu.
– El bucle principal refresca cada 60 s con actualización completa; la e‑Paper es lenta por naturaleza, y un minuto es un buen equilibrio entre estática y dinámica. Se puede ajustar.

Compilación/flash/ejecución

No hay “flash” como tal; es Python. Aun así, fijamos un entorno aislado, versiones exactas y un servicio opcional para arranque automático.

1) Crear proyecto y entorno virtual

  • mkdir -p ~/spi-epaper-live-dashboard
  • cd ~/spi-epaper-live-dashboard
  • python3.11 -m venv .venv
  • source .venv/bin/activate
  • python -m pip install –upgrade pip==24.2 setuptools==72.1.0 wheel==0.44.0
  • python -m pip install spidev==3.6 lgpio==0.2.2.0 gpiozero==2.0 Pillow==10.4.0 psutil==5.9.8 requests==2.32.3

Verifica versiones:
– python -V
– python -c «import spidev, lgpio, gpiozero, PIL, psutil, requests; print(‘OK’)»

2) Crear los archivos con el código

  • nano epaper29.py
  • Pega el contenido del driver epaper29.py y guarda.
  • nano dashboard.py
  • Pega el contenido de dashboard.py y guarda.

3) Habilitar el backend GPIO adecuado

Para Raspberry Pi 5 en Bookworm, usa gpiozero con la factoría lgpio. Exporta la variable antes de ejecutar:

  • export GPIOZERO_PIN_FACTORY=lgpio

Para hacerlo persistente en el shell actual:
– echo ‘export GPIOZERO_PIN_FACTORY=lgpio’ >> ~/.bashrc
– source ~/.bashrc

(Esta exportación garantiza que gpiozero no intente usar el backend RPi.GPIO clásico, que en Pi 5/Bookworm ya no es la opción recomendada.)

4) Probar el driver (prueba en seco)

  • ls -l /dev/spidev0.0
  • python -c «import spidev; d=spidev.SpiDev(); d.open(0,0); print(d.max_speed_hz); d.close()»

Si esto funciona, el bus SPI está operativo.

5) Ejecutar la aplicación del dashboard

  • cd ~/spi-epaper-live-dashboard
  • source .venv/bin/activate
  • export GPIOZERO_PIN_FACTORY=lgpio
  • python dashboard.py

La pantalla debería:
1) Inicializarse (parpadeo típico de e‑Paper),
2) Limpiarse a blanco y
3) Mostrar el tablero con hora, IP, CPU, carga, RAM, disco y tráfico de red.

Se refrescará cada 60 s.

6) Ejecutarlo como servicio (opcional, arranque automático)

Crea un servicio systemd para que se inicie tras el boot.

  • sudo nano /etc/systemd/system/spi-epaper-live-dashboard.service

Contenido (ajusta “User” si procede):

[Unit]
Description=SPI e-Paper Live Dashboard (Raspberry Pi 5 + Waveshare 2.9" e-Paper HAT)
After=network-online.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/spi-epaper-live-dashboard
Environment=GPIOZERO_PIN_FACTORY=lgpio
Environment=PYTHONUNBUFFERED=1
ExecStart=/home/pi/spi-epaper-live-dashboard/.venv/bin/python /home/pi/spi-epaper-live-dashboard/dashboard.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Habilita e inicia:
– sudo systemctl daemon-reload
– sudo systemctl enable spi-epaper-live-dashboard.service
– sudo systemctl start spi-epaper-live-dashboard.service
– sudo systemctl status spi-epaper-live-dashboard.service

Para ver logs:
– journalctl -u spi-epaper-live-dashboard.service -f

Validación paso a paso

1) Comprobar que SPI está activo:
– ls -l /dev/spidev0.0
– Debe existir el dispositivo de caracteres.

2) Verificar dependencias en el venv:
– source ~/spi-epaper-live-dashboard/.venv/bin/activate
– python -c «import spidev, gpiozero, PIL, psutil; print(‘deps OK’)»

3) Exportar la factoría correcta de GPIO:
– export GPIOZERO_PIN_FACTORY=lgpio
– python -c «from gpiozero import LED; print(‘gpiozero OK’)»

4) Ejecución de dashboard:
– python dashboard.py
– Observa un refresco inicial y luego el layout. El parpadeo fuerte indica actualización completa; los textos deben ser nítidos.

5) Confirmar datos:
– La hora debe coincidir con la del sistema.
– La IP debe ser la de la interfaz activa (evita 127.0.0.1; si la ves, revisa conectividad).
– CPU% y Load deben variar si ejecutas una carga: por ejemplo, en otra terminal:
– yes > /dev/null &
– Observa la subida de CPU% tras uno o dos ciclos de 60 s.
– Luego mata la carga: killall yes
– RAM y disco: deben coincidir con free -h y df -h.
– Temperatura: sube con carga sostenida; ver también:
– vcgencmd measure_temp

6) Validación de servicio systemd:
– sudo systemctl status spi-epaper-live-dashboard.service
– Debe estar “active (running)”.
– Tras reiniciar (sudo reboot), a los 30–60 s el dashboard debe estar visible sin intervención.

Troubleshooting

1) No aparece /dev/spidev0.0
– Causa: SPI no habilitado o dtparam ausente.
– Solución:
– sudo raspi-config → Interface Options → SPI → Enable → Reboot
– Verifica /boot/firmware/config.txt contenga dtparam=spi=on

2) Permisos o error: cannot open SPI device
– Causa: usuario sin permisos o dispositivo en uso.
– Solución:
– Ejecuta con usuario estándar pero con pertenencia a grupo “spi” (normalmente ya configurado en Raspberry Pi OS).
– Revisa que otro proceso no haya abierto SPI.

3) El script denuncia GPIO: “No pin factory found” o “RPi.GPIO missing”
– Causa: gpiozero usando backend incorrecto en Bookworm.
– Solución:
– export GPIOZERO_PIN_FACTORY=lgpio
– Instala lgpio en el venv: pip install lgpio==0.2.2.0
– Evita depender de RPi.GPIO en Pi 5/Bookworm.

4) Pantalla no refresca o se queda en blanco
– Causas probables:
– Pines DC/RST/BUSY diferentes a los del código.
– HAT mal asentado o invertido.
– Frecuencia SPI demasiado alta para tu cableado.
– Soluciones:
– Verifica mapeo de pines: DC=GPIO25, RST=GPIO17, BUSY=GPIO24.
– Reduce frecuencia a 2 MHz: en EPaper29(…, spi_hz=2_000_000).
– Revisa que el HAT esté correctamente acoplado y con 3V3/GND operativos.

5) Ghosting o artefactos tras mucho tiempo
– Causa: actualizaciones parciales (no usadas aquí) o falta de borrados completos.
– Solución:
– El driver usa update completo por defecto; si ves ghosting, fuerza un clear() cada N ciclos.
– Aumenta descansos entre refrescos si la temperatura ambiente es baja.

6) Fuentes no encontradas
– Causa: ruta de TTF distinta.
– Solución:
– Verifica que existan las rutas en /usr/share/fonts/truetype/dejavu/.
– Cambia a ImageFont.load_default() si falta la fuente:
– Reemplaza las cargas de TTF por ImageFont.load_default() temporalmente.

7) Temperatura no aparece
– Causa: psutil no expone sensor en tu kernel/firmware.
– Solución:
– El código intenta vcgencmd; instala firmware tools si faltan:
– sudo apt install -y libraspberrypi-bin
– Reintenta.

8) Error al iniciar como servicio systemd
– Causa: ruta de venv o WorkingDirectory incorrecta.
– Solución:
– Revisa ExecStart y WorkingDirectory en el unit file.
– journalctl -u spi-epaper-live-dashboard.service -f para ver el traceback exacto.

Mejoras/variantes

  • Actualización parcial de regiones:
  • Para paneles 2.9″ V2 es posible usar partial updates para refrescar solo números (CPU, hora) con menor parpadeo y mayor frecuencia (por ejemplo cada 10 s) y un full update cada 5–10 minutos.
  • Requiere añadir la ventana parcial (comando PARTIAL_WINDOW 0x90) y escribir solo esa región. Debes mantener LUTs adecuados si el controlador lo exige.

  • Modo bajo consumo:

  • Si el panel solo debe refrescar unas pocas veces por hora, puedes llamar a sleep() tras cada actualización y re‑init() antes de la siguiente. Esto reduce consumo y ghosting.
  • Ten en cuenta el tiempo adicional de init.

  • KPI remotos:

  • Integra requests para consultar métricas de un servicio REST/MQTT (por ejemplo, estado de un pipeline CI, ocupación de colas, o SLA externo) y visualízalas en una banda inferior.

  • Diseño alternativo:

  • Cambia a orientación horizontal (128×296) rotando la composición y ajustando el driver.
  • Emplea tipografías condensadas para maximizar información.
  • Añade iconografía minimalista (dibujada en 1bpp) para CPU, red, disco.

  • Programación de refrescos inteligente:

  • Si la IP no cambia y el sistema está idle, aumenta el intervalo a 2–5 minutos.
  • Reduce el intervalo si load1 supera un umbral.

  • Exportación de logs:

  • Genera un log con los valores mostrados para correlacionar con eventos del sistema (spikes de temperatura, caídas de red).

Checklist de verificación

Marca cada punto al avanzar:

  • [ ] Raspberry Pi OS Bookworm 64‑bit instalado y actualizado (sudo apt full-upgrade).
  • [ ] SPI habilitado (dtparam=spi=on) y /dev/spidev0.0 visible.
  • [ ] HAT “Waveshare 2.9″ e‑Paper HAT” correctamente insertado en la Raspberry Pi 5.
  • [ ] Proyecto creado en ~/spi-epaper-live-dashboard.
  • [ ] Entorno virtual .venv creado con Python 3.11 y pip 24.2.
  • [ ] Paquetes instalados en venv: spidev 3.6, lgpio 0.2.2.0, gpiozero 2.0, Pillow 10.4.0, psutil 5.9.8, requests 2.32.3.
  • [ ] Variable de entorno GPIOZERO_PIN_FACTORY=lgpio exportada.
  • [ ] Archivo epaper29.py creado y sin errores de importación.
  • [ ] Archivo dashboard.py creado y ejecuta sin fallos.
  • [ ] La pantalla muestra el dashboard y refresca cada 60 s con datos coherentes.
  • [ ] Servicio systemd creado y en estado “active (running)” (opcional).
  • [ ] Validación cruzada de métricas (free -h, df -h, vcgencmd measure_temp) coincide con lo mostrado.

Con este flujo, habrás construido un “spi-epaper-live-dashboard” estable y reproducible sobre “Raspberry Pi 5 + Waveshare 2.9″ e‑Paper HAT”, usando Raspberry Pi OS Bookworm 64‑bit, Python 3.11 y la toolchain fijada. El proyecto queda listo para evolucionar hacia paneles de control más ricos (KPI de servicios, alertas, modos nocturnos, parcial updates) manteniendo las mismas bases de SPI y renderizado monocromo con Pillow.

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 sistema operativo utilizado en el proyecto?




Pregunta 2: ¿Qué versión de Python se utiliza en el entorno?




Pregunta 3: ¿Cuál es la versión de pip que debe instalarse?




Pregunta 4: ¿Qué herramienta se utiliza para acceder a /dev/spidevX.Y?




Pregunta 5: ¿Qué versión de lgpio se requiere en el entorno virtual?




Pregunta 6: ¿Cuál es la versión de Pillow que se debe instalar?




Pregunta 7: ¿Qué biblioteca se utiliza para obtener información del sistema como CPU y RAM?




Pregunta 8: ¿Qué versión de requests se debe utilizar en el entorno?




Pregunta 9: ¿Qué componente proporciona GPIO de alto nivel sobre lgpio?




Pregunta 10: ¿Qué es un 'spi-epaper-live-dashboard'?




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