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
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.



