Alarma de calidad del aire con Raspberry Pi 4 usando BME680 y HyperPixel 4.0 Touch
Objetivo y caso de uso
Qué construirás: Un panel de calidad del aire con Raspberry Pi 4 Model B usando un sensor BME680 y una pantalla HyperPixel 4.0 Touch para mostrar las condiciones de la habitación en vivo y estimar IAQ, eCO2 y TVOC en tiempo real. La interfaz se ejecuta localmente con tasas de refresco fluidas para pantalla táctil y muestra una alarma clara en pantalla cuando el eCO2 estimado supera un umbral configurable, como 1000 ppm.
Por qué importa / Casos de uso
- Recordatorio de ventilación para aulas que ofrece una indicación visible cuando el eCO2 estimado supera 1000–1500 ppm.
- Monitor de confort para oficina en casa para seguir temperatura, humedad y tendencias de aire viciado con actualizaciones locales de baja latencia.
- Indicador para taller o sala de hobbies para detectar una mala renovación del aire y cambios en los patrones de VOC durante proyectos en interiores.
- Pantalla de estado de sala de reuniones con estado codificado por colores para comprobaciones rápidas de apto/no apto antes de la ocupación.
- Prototipo educativo para comparar cambios de ventilación frente a métricas IAQ estimadas y el comportamiento de la alarma.
Resultado esperado
- Interfaz de pantalla táctil que muestra temperatura, humedad, presión, resistencia del gas, puntuación IAQ, eCO2 estimado y TVOC estimado.
- Estado de la habitación codificado por colores con renderizado responsivo, normalmente alrededor de 20–30 FPS en una Pi 4 para un panel ligero.
- Banner de alarma visible cuando el eCO2 estimado cruza el límite configurado, con respuesta local de la interfaz en menos de un segundo.
- Interacción por toque o ratón para alternar detalles y reconocer temporalmente la alarma sin detener la monitorización.
- Soporte tanto para modo con hardware real como para modo simulado para probar el flujo de la interfaz, umbrales de alarma y datos de demostración.
- Funcionamiento local eficiente adecuado para uso tipo kiosco, manteniéndose a menudo en un rango modesto de GPU para un panel simple a pantalla completa.
Audiencia: Makers de Raspberry Pi, educadores y aficionados que construyen pantallas ambientales prácticas; Nivel: Intermedio
Arquitectura/flujo: La Pi 4 lee los valores del sensor BME680, deriva IAQ además de eCO2/TVOC estimados, actualiza un panel HyperPixel Touch a pantalla completa, aplica reglas de estado por color y activa un banner de alarma visible con reconocimiento táctil opcional cuando se supera el umbral configurado; en modo simulado, las lecturas simuladas siguen la misma canalización.
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: 31 apartados, 1 tablas y 23 bloques de código detectados en el contenido publicado.
- Código comprobado: 2 Python/py_compile, 20 Bash/copy-paste checks.
- Catálogo soportado: el texto se contrastó contra los perfiles de dispositivo validables de Prometeo; 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
Nota de seguridad
Este proyecto es un prototipo educativo de calidad del aire en interiores, no un instrumento de seguridad certificado.
- El BME680 no mide CO2 real directamente.
- Los valores de eCO2 e IAQ mostrados en este tutorial son estimaciones heurísticas derivadas del comportamiento de la humedad y la resistencia del gas.
- No uses esta construcción como única base para decisiones de salud, cumplimiento legal, detección de atmósferas peligrosas o respuesta a emergencias.
- Alimenta la Raspberry Pi solo con una fuente USB-C adecuada de bajo voltaje.
- Usa una carcasa o un montaje seguro para que la electrónica expuesta no pueda cortocircuitarse ni tocarse accidentalmente.
Requisitos previos
Necesitas:
- Raspberry Pi 4 Model B
- Raspberry Pi OS Bookworm de 64 bits
- Python 3.11
- Módulo BME680 con soporte I2C
- HyperPixel 4.0 Touch
- Uso básico de terminal
- Conocimientos básicos de cableado GPIO/I2C
Materiales
| Elemento | Modelo exacto | Propósito |
|---|---|---|
| Ordenador de placa única | Raspberry Pi 4 Model B | Ejecuta la aplicación |
| Sensor | BME680 | Temperatura, humedad, presión, resistencia del gas |
| Pantalla táctil | HyperPixel 4.0 Touch | Interfaz táctil local |
| Fuente de alimentación | Fuente USB-C de 5 V para Raspberry Pi 4 | Alimentación estable |
| Cableado | Cables jumper hembra a hembra | Cableado del sensor |
| Almacenamiento | Tarjeta microSD, 16 GB o superior | SO y archivos del proyecto |
Configuración del hardware
Cableado I2C del BME680
Conecta el BME680 al encabezado de 40 pines de la Raspberry Pi:
- BME680 VCC/VIN -> 3.3 V en la Raspberry Pi, pin 1 físico
- BME680 GND -> GND en la Raspberry Pi, pin 6 físico
- BME680 SDA -> SDA1 en la Raspberry Pi, pin 3 físico
- BME680 SCL -> SCL1 en la Raspberry Pi, pin 5 físico
Notas:
- Confirma que tu placa breakout es compatible con 3.3 V
- No conectes una placa solo de 3.3 V a 5 V
- Si la placa también soporta SPI, usa solo los pines I2C listados arriba
HyperPixel 4.0 Touch
Instala y verifica la HyperPixel usando las instrucciones actuales de Pimoroni para tu imagen de Raspberry Pi OS. Antes de continuar, confirma:
- La pantalla muestra el escritorio de Raspberry Pi
- La entrada táctil funciona correctamente
Preparación del sistema
Actualiza la Pi y habilita I2C:
sudo apt update
sudo apt full-upgrade -y
sudo raspi-config
En raspi-config:
- Abre Interface Options
- Habilita I2C
- Reinicia si se te solicita
Comprueba Python:
python3 --version
Instala los paquetes del sistema necesarios:
sudo apt install -y git python3-pip python3-venv i2c-tools
Verifica que el sensor aparece en I2C:
i2cdetect -y 1
Un BME680 aparece habitualmente en 0x76 o 0x77.
Configuración del proyecto
Crea el proyecto y el entorno virtual:
mkdir -p ~/projects/touchscreen-co2-air-quality-alarm
cd ~/projects/touchscreen-co2-air-quality-alarm
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install pygame smbus2 bme680
Valida las importaciones:
python3 -c "import pygame, smbus2, bme680; print('imports ok')"
Código de la aplicación
air_quality_alarm.py
#!/usr/bin/env python3
"""
Touchscreen air-quality alarm for:
- Raspberry Pi 4 Model B
- BME680 over I2C
- HyperPixel 4.0 Touch
The IAQ, eCO2, and TVOC values here are educational heuristic estimates.
They are useful for trend display and UI behavior validation, not certified measurement.
"""
from __future__ import annotations
import argparse
import math
import random
import sys
import time
from dataclasses import dataclass
from typing import Optional, Tuple
import pygame
@dataclass
class SensorReading:
temperature_c: float
humidity_pct: float
pressure_hpa: float
gas_ohms: float
iaq_score: float
eco2_ppm: int
tvoc_ppb: int
timestamp: float
def clamp(value: float, low: float, high: float) -> float:
return max(low, min(high, value))
def estimate_iaq_score(humidity_pct: float, gas_ohms: float) -> float:
"""
Educational heuristic only.
Lower score is better, with an approximate 0..500 scale.
"""
humidity_target = 40.0
humidity_offset = abs(humidity_pct - humidity_target)
humidity_score = clamp(humidity_offset / 40.0 * 25.0, 0.0, 25.0)
gas_reference = 50000.0
gas_ratio = clamp((gas_reference - gas_ohms) / gas_reference, 0.0, 1.0)
gas_score = gas_ratio * 475.0
return round(humidity_score + gas_score, 1)
def estimate_eco2_ppm(iaq_score: float, gas_ohms: float) -> int:
"""
Educational heuristic only.
Maps worsening gas behavior to an estimated eCO2 range.
"""
base = 420
score_component = int(iaq_score * 4.2)
gas_component = int(clamp((50000.0 - gas_ohms) / 80.0, 0.0, 2000.0))
return int(clamp(base + score_component + gas_component, 400, 2500))
def estimate_tvoc_ppb(iaq_score: float, gas_ohms: float) -> int:
"""
Educational heuristic only.
"""
base = 20
score_component = int(iaq_score * 1.5)
gas_component = int(clamp((50000.0 - gas_ohms) / 120.0, 0.0, 1000.0))
return int(clamp(base + score_component + gas_component, 0, 1200))
def air_quality_state(eco2_ppm: int) -> Tuple[str, Tuple[int, int, int]]:
if eco2_ppm < 800:
return ("GOOD", (40, 140, 70))
if eco2_ppm < 1200:
return ("ELEVATED", (180, 150, 40))
if eco2_ppm < 1600:
return ("POOR", (190, 100, 30))
return ("ALARM", (170, 40, 40))
class BME680Adapter:
"""Real sensor adapter using the bme680 package."""
def __init__(self, i2c_addr: int = 0x76):
self.i2c_addr = i2c_addr
self.sensor = None
def open(self) -> None:
import bme680
self.sensor = bme680.BME680(i2c_addr=self.i2c_addr)
self.sensor.set_humidity_oversample(bme680.OS_2X)
self.sensor.set_pressure_oversample(bme680.OS_4X)
self.sensor.set_temperature_oversample(bme680.OS_8X)
self.sensor.set_filter(bme680.FILTER_SIZE_3)
self.sensor.set_gas_status(bme680.ENABLE_GAS_MEAS)
self.sensor.set_gas_heater_temperature(320)
self.sensor.set_gas_heater_duration(150)
self.sensor.select_gas_heater_profile(0)
def read(self) -> SensorReading:
if self.sensor is None:
raise RuntimeError("Sensor not opened")
if not self.sensor.get_sensor_data():
raise RuntimeError("No sensor data available")
data = self.sensor.data
gas_ohms = float(getattr(data, "gas_resistance", 0.0) or 0.0)
iaq_score = estimate_iaq_score(float(data.humidity), gas_ohms)
eco2_ppm = estimate_eco2_ppm(iaq_score, gas_ohms)
tvoc_ppb = estimate_tvoc_ppb(iaq_score, gas_ohms)
return SensorReading(
temperature_c=float(data.temperature),
humidity_pct=float(data.humidity),
pressure_hpa=float(data.pressure),
gas_ohms=gas_ohms,
iaq_score=iaq_score,
eco2_ppm=eco2_ppm,
tvoc_ppb=tvoc_ppb,
timestamp=time.time(),
)
class MockBME680Adapter:
"""Mock adapter for validation without hardware."""
def __init__(self) -> None:
self.t0 = time.time()
def open(self) -> None:
return
def read(self) -> SensorReading:
elapsed = time.time() - self.t0
temperature_c = 22.0 + 1.5 * math.sin(elapsed / 40.0)
humidity_pct = 45.0 + 8.0 * math.sin(elapsed / 55.0)
pressure_hpa = 1012.0 + 1.2 * math.sin(elapsed / 120.0)
baseline = 45000.0
drop = min(32000.0, elapsed * 350.0)
noise = random.uniform(-1200.0, 1200.0)
gas_ohms = max(5000.0, baseline - drop + noise)
iaq_score = estimate_iaq_score(humidity_pct, gas_ohms)
eco2_ppm = estimate_eco2_ppm(iaq_score, gas_ohms)
tvoc_ppb = estimate_tvoc_ppb(iaq_score, gas_ohms)
return SensorReading(
temperature_c=temperature_c,
humidity_pct=humidity_pct,
pressure_hpa=pressure_hpa,
gas_ohms=gas_ohms,
iaq_score=iaq_score,
eco2_ppm=eco2_ppm,
tvoc_ppb=tvoc_ppb,
timestamp=time.time(),
)
class HyperPixelUI:
def __init__(self, width: int = 800, height: int = 480):
pygame.init()
pygame.font.init()
self.screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Air Quality Alarm")
self.width = width
self.height = height
self.font_big = pygame.font.SysFont("Arial", 52)
self.font_med = pygame.font.SysFont("Arial", 32)
self.font_small = pygame.font.SysFont("Arial", 24)
self.show_detail = False
self.alarm_ack_until = 0.0
def draw(self, reading: SensorReading, alarm_threshold: int) -> None:
state, bg = air_quality_state(reading.eco2_ppm)
self.screen.fill(bg)
now = time.time()
alarm_active = (
reading.eco2_ppm >= alarm_threshold and now >= self.alarm_ack_until
)
title = self.font_big.render(state, True, (255, 255, 255))
self.screen.blit(title, (30, 20))
eco2_text = self.font_big.render(
f"eCO2 {reading.eco2_ppm} ppm", True, (255, 255, 255)
)
self.screen.blit(eco2_text, (30, 95))
iaq_text = self.font_med.render(
f"IAQ score: {reading.iaq_score:.1f}", True, (255, 255, 255)
)
self.screen.blit(iaq_text, (30, 170))
temp_text = self.font_small.render(
f"Temp: {reading.temperature_c:.1f} C", True, (255, 255, 255)
)
hum_text = self.font_small.render(
f"Humidity: {reading.humidity_pct:.1f} %", True, (255, 255, 255)
)
pres_text = self.font_small.render(
f"Pressure: {reading.pressure_hpa:.1f} hPa", True, (255, 255, 255)
)
gas_text = self.font_small.render(
f"Gas: {reading.gas_ohms:.0f} ohms", True, (255, 255, 255)
)
tvoc_text = self.font_small.render(
f"TVOC est: {reading.tvoc_ppb} ppb", True, (255, 255, 255)
)
self.screen.blit(temp_text, (30, 230))
self.screen.blit(hum_text, (30, 265))
self.screen.blit(pres_text, (30, 300))
if self.show_detail:
self.screen.blit(gas_text, (30, 335))
self.screen.blit(tvoc_text, (30, 370))
info = self.font_small.render(
"Tap left: details | Tap right: acknowledge 60 s",
True,
(255, 255, 255),
)
self.screen.blit(info, (20, 440))
if alarm_active:
banner = pygame.Rect(500, 20, 260, 90)
pygame.draw.rect(self.screen, (255, 230, 230), banner, border_radius=12)
msg1 = self.font_med.render("VENTILATE ROOM", True, (120, 0, 0))
msg2 = self.font_small.render(
f"Threshold {alarm_threshold} ppm exceeded", True, (120, 0, 0)
)
self.screen.blit(msg1, (520, 35))
self.screen.blit(msg2, (520, 75))
pygame.display.flip()
def handle_event(self, event: pygame.event.Event) -> None:
if event.type == pygame.MOUSEBUTTONDOWN:
x, _y = event.pos
if x < self.width // 2:
self.show_detail = not self.show_detail
else:
self.alarm_ack_until = time.time() + 60.0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Touchscreen CO2 air-quality alarm")
parser.add_argument("--mock", action="store_true", help="Run without hardware")
parser.add_argument(
"--i2c-addr",
default="0x76",
help="BME680 I2C address such as 0x76 or 0x77",
)
parser.add_argument(
"--alarm-ppm",
type=int,
default=1200,
help="Estimated eCO2 alarm threshold",
)
parser.add_argument(
"--interval",
type=float,
default=2.0,
help="Seconds between sensor updates",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
i2c_addr = int(args.i2c_addr, 16)
sensor = MockBME680Adapter() if args.mock else BME680Adapter(i2c_addr=i2c_addr)
sensor.open()
ui = HyperPixelUI()
last_read = 0.0
reading: Optional[SensorReading] = None
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
ui.handle_event(event)
now = time.time()
if reading is None or (now - last_read) >= args.interval:
reading = sensor.read()
last_read = now
if reading is not None:
ui.draw(reading, args.alarm_ppm)
time.sleep(0.05)
pygame.quit()
return 0
if __name__ == "__main__":
sys.exit(main())
test_logic.py
#!/usr/bin/env python3
import air_quality_alarm as app
def run() -> None:
assert app.clamp(5, 0, 10) == 5
assert app.clamp(-1, 0, 10) == 0
assert app.clamp(11, 0, 10) == 10
iaq_good = app.estimate_iaq_score(40.0, 50000.0)
iaq_bad = app.estimate_iaq_score(70.0, 10000.0)
assert iaq_good <= iaq_bad
eco2_good = app.estimate_eco2_ppm(iaq_good, 50000.0)
eco2_bad = app.estimate_eco2_ppm(iaq_bad, 10000.0)
assert eco2_good < eco2_bad
assert app.air_quality_state(700)[0] == "GOOD"
assert app.air_quality_state(900)[0] == "ELEVATED"
assert app.air_quality_state(1300)[0] == "POOR"
assert app.air_quality_state(1800)[0] == "ALARM"
mock = app.MockBME680Adapter()
mock.open()
reading = mock.read()
assert 0 <= reading.iaq_score <= 500
assert 400 <= reading.eco2_ppm <= 2500
assert reading.pressure_hpa > 800
print("logic tests passed")
if __name__ == "__main__":
run()
Guarda los archivos
cd ~/projects/touchscreen-co2-air-quality-alarm
chmod 700 .
nano air_quality_alarm.py
nano test_logic.py
chmod +x air_quality_alarm.py test_logic.py
Pasos de validación
1. Validar sintaxis
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 -m py_compile air_quality_alarm.py test_logic.py
Evidencia esperada:
- Sin salida de
py_compile
2. Validar pruebas lógicas
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 test_logic.py
Evidencia esperada:
logic tests passed
3. Validar modo simulado
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --mock --alarm-ppm 1200 --interval 1.5
Evidencia esperada:
- Se abre una ventana de aproximadamente 800 x 480
- Los valores se actualizan cada pocos segundos
- El color de fondo cambia a medida que la habitación simulada empeora
- Hacer clic en la mitad izquierda alterna los detalles
- Hacer clic en la mitad derecha reconoce la alarma durante 60 segundos
- Tras suficiente tiempo simulado, aparece el banner VENTILATE ROOM
4. Validar modo con sensor real
Si el sensor está en 0x76:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --alarm-ppm 1200 --interval 2.0
Si el sensor está en 0x77:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --i2c-addr 0x77 --alarm-ppm 1200 --interval 2.0
Evidencia esperada:
- La temperatura es plausible para la habitación
- La humedad es plausible para la habitación
- La presión está en un rango atmosférico normal
- La resistencia del gas es distinta de cero
- La pantalla táctil se actualiza continuamente
5. Validación práctica en una habitación
Para validar el objetivo principal del tutorial, prueba el dispositivo en una habitación real:
- Coloca el dispositivo en una habitación pequeña y cerrada
- Deja la puerta y las ventanas cerradas durante un tiempo
- Observa si la tendencia del eCO2 estimado sube con el tiempo
- Abre una puerta o ventana
- Observa si la tendencia mostrada empieza a mejorar en las actualizaciones posteriores
Evidencia esperada:
- El estado es fácil de interpretar en pantalla
- La alarma visible aparece cuando se supera el umbral estimado configurado
- Los cambios de ventilación producen un cambio de tendencia visible
Esto valida el prototipo como un recordatorio educativo para habitaciones basado en tendencias, no como un medidor de CO2 certificado.
Comandos de ejecución
Modo simulado:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --mock --alarm-ppm 1200 --interval 1.5
Modo con hardware real y dirección predeterminada:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --alarm-ppm 1200 --interval 2.0
Modo con hardware real y dirección alternativa:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --i2c-addr 0x77 --alarm-ppm 1200 --interval 2.0
Script lanzador opcional
cat > ~/projects/touchscreen-co2-air-quality-alarm/run.sh << 'EOF'
#!/bin/sh
set -eu
cd "$HOME/projects/touchscreen-co2-air-quality-alarm"
. .venv/bin/activate
exec python3 air_quality_alarm.py --alarm-ppm 1200 --interval 2.0
EOF
chmod +x ~/projects/touchscreen-co2-air-quality-alarm/run.sh
Resolución de problemas
El sensor no es visible en i2cdetect
Vuelve a comprobar:
- I2C habilitado en
raspi-config - Alimentación correcta de 3.3 V
- SDA y SCL no están intercambiados
- Conexión de tierra sólida
- Dirección
0x76frente a0x77
Comandos útiles:
i2cdetect -y 1
python3 air_quality_alarm.py --i2c-addr 0x77
Errores de importación
Activa el entorno virtual y reinstala:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 -m pip install pygame smbus2 bme680
La interfaz no aparece en la HyperPixel
Comprueba:
- La HyperPixel es la pantalla activa del escritorio
- Estás ejecutando desde una sesión de escritorio local, no desde una shell SSH sin interfaz
- Los controladores de pantalla y táctil están instalados correctamente
Los valores parecen irreales
Posibles razones:
- Tiempo de calentamiento del sensor
- Corrientes de aire directas o calor corporal cerca
- Umbral demasiado agresivo para tu habitación
Prueba un umbral de alarma más alto:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --alarm-ppm 1400 --interval 2.0
Para un comportamiento más estricto:
cd ~/projects/touchscreen-co2-air-quality-alarm
. .venv/bin/activate
python3 air_quality_alarm.py --alarm-ppm 1000 --interval 2.0
Lista de comprobación final
- [ ] Raspberry Pi 4 Model B está ejecutando Raspberry Pi OS Bookworm de 64 bits
- [ ] Python 3.11 está instalado
- [ ] HyperPixel 4.0 Touch muestra el escritorio y acepta entrada táctil
- [ ] El BME680 está cableado correctamente a 3.3 V, GND, SDA y SCL
- [ ]
i2cdetect -y 1muestra el sensor en0x76o0x77 - [ ] El entorno virtual está creado
- [ ] Las dependencias se instalan correctamente
- [ ]
python3 -m py_compile air_quality_alarm.py test_logic.pyse ejecuta correctamente - [ ]
python3 test_logic.pyimprimelogic tests passed - [ ] El modo simulado abre la interfaz y muestra valores cambiantes
- [ ] El toque en el lado izquierdo alterna las métricas detalladas
- [ ] El toque en el lado derecho reconoce la alarma durante 60 segundos
- [ ] El modo real muestra valores del sensor en vivo
- [ ] La alarma visible aparece cuando se cruza el umbral estimado
- [ ] Los cambios de ventilación causan un cambio de tendencia visible
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.




