Un monitor de calidad del aire interior que mide el CO2 y las partículas finas te indica cuándo una sala necesita aire fresco o filtración mucho antes de que la gente se sienta cansada o empiece a toser. En este tutorial construirás uno con un ESP32, un sensor de CO2 Sensirion SCD41 y un sensor de partículas Plantower PMS5003, publicarás las lecturas como JSON por MQTT y configurarás alertas de ventilación en un broker de ConnectMQ.
Qué miden los sensores
- El CO2 (ppm) es el mejor indicador de la ventilación en salas ocupadas. El aire exterior ronda los 400 a 450 ppm. Las recomendaciones de ventilación de edificios suelen usar valores de alrededor de 800 a 1.000 ppm como señal de que el aporte de aire fresco es insuficiente para el número de personas en la sala. El SCD41 es un verdadero sensor NDIR fotoacústico, no una estimación a partir de lecturas de COV.
- El PM2.5 (microgramos por metro cúbico) mide las partículas finas procedentes de cocinar, velas, la contaminación exterior o el polvo de obra. El PMS5003 usa dispersión láser e informa de PM1.0, PM2.5 y PM10.
- La temperatura y la humedad vienen incluidas con el SCD41 y aportan contexto sobre el confort.
La detección de COV es una ampliación útil. Un Sensirion SGP41 o un Bosch BME680 añade un índice de COV que reacciona a los productos de limpieza y a las emisiones de los materiales. Se ha dejado fuera del montaje principal para mantener el código breve.
Componentes y cableado
| Componente | Pin | Pin del ESP32 | Notas |
|---|---|---|---|
| SCD41 | VDD | 3V3 | Las placas de conexión suelen incluir resistencias pull-up |
| SCD41 | GND | GND | |
| SCD41 | SDA | GPIO 21 | Datos I2C |
| SCD41 | SCL | GPIO 22 | Reloj I2C |
| PMS5003 | VCC | 5V (VIN) | El ventilador necesita 5 V |
| PMS5003 | GND | GND | |
| PMS5003 | TX | GPIO 16 (RX2) | El nivel lógico de datos del sensor es de 3,3 V |
| PMS5003 | RX | GPIO 17 (TX2) | Opcional, solo para comandos |
Usa el Arduino IDE 2.x con el paquete de placas esp32 (core 3.x) e instala Sensirion I2C SCD4x (versión 1.x) y PubSubClient desde el gestor de librerías. La trama del PMS5003 se analiza directamente en el sketch, así que no hace falta ninguna librería adicional.
Coloca el monitor a la altura de la respiración, lejos de ventanas, puertas y difusores de impulsión, y mantén despejada la entrada de aire del PMS5003. Un sensor junto a una ventana abierta mide el exterior, no la sala.
El sketch completo
#include <WiFi.h>
#include <Wire.h>
#include <PubSubClient.h>
#include <SensirionI2cScd4x.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
// Broker público de pruebas. En producción usa 8883 + WiFiClientSecure y las credenciales del portal.
const char* BROKER = "broker.connectmq.com";
const int PORT = 1883;
const char* TOPIC = "connectmq/demo/iaq-3hv9/office-201/air";
const unsigned long PUBLISH_MS = 30000;
WiFiClient net;
PubSubClient mqtt(net);
SensirionI2cScd4x scd4x;
HardwareSerial pmsSerial(2);
uint16_t co2 = 0;
float tempC = NAN, rh = NAN;
uint16_t pm1 = 0, pm25 = 0, pm10 = 0;
bool pmValid = false;
unsigned long lastPublish = 0;
// Lee una trama de 32 bytes del PMS5003: 0x42 0x4D, longitud, 13 palabras de datos, suma de comprobación
bool readPms() {
static uint8_t buf[32];
while (pmsSerial.available() >= 32) {
if (pmsSerial.peek() != 0x42) { pmsSerial.read(); continue; }
pmsSerial.readBytes(buf, 32);
if (buf[1] != 0x4D) continue;
uint16_t sum = 0;
for (int i = 0; i < 30; i++) sum += buf[i];
uint16_t check = (buf[30] << 8) | buf[31];
if (sum != check) continue;
// valores de "entorno atmosférico"
pm1 = (buf[10] << 8) | buf[11];
pm25 = (buf[12] << 8) | buf[13];
pm10 = (buf[14] << 8) | buf[15];
return true;
}
return false;
}
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
}
void connectMqtt() {
while (!mqtt.connected()) {
String id = "iaq-" + String((uint32_t)ESP.getEfuseMac(), HEX);
if (!mqtt.connect(id.c_str())) delay(3000);
}
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
pmsSerial.begin(9600, SERIAL_8N1, 16, 17);
scd4x.begin(Wire, SCD41_I2C_ADDR_62);
scd4x.stopPeriodicMeasurement(); // por si seguía midiendo tras un reinicio por software
delay(500);
if (scd4x.startPeriodicMeasurement() != 0) {
Serial.println("SCD41 not found, check wiring");
}
connectWifi();
mqtt.setServer(BROKER, PORT);
mqtt.setBufferSize(512);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (readPms()) pmValid = true;
bool ready = false;
if (scd4x.getDataReadyStatus(ready) == 0 && ready) {
uint16_t c; float t, h;
if (scd4x.readMeasurement(c, t, h) == 0 && c != 0) {
co2 = c; tempC = t; rh = h;
}
}
if (millis() - lastPublish >= PUBLISH_MS && co2 > 0) {
lastPublish = millis();
char msg[256];
snprintf(msg, sizeof(msg),
"{\"co2_ppm\":%u,\"temp_c\":%.1f,\"rh\":%.1f,\"pm1\":%u,\"pm25\":%u,\"pm10\":%u,\"pm_valid\":%s,\"rssi\":%d}",
co2, tempC, rh, pm1, pm25, pm10, pmValid ? "true" : "false", WiFi.RSSI());
mqtt.publish(TOPIC, msg);
Serial.println(msg);
}
}
En modo periódico, el SCD41 produce una medición nueva cada cinco segundos, y el PMS5003 envía tramas aproximadamente una vez por segundo. El sketch conserva los últimos valores y publica cada 30 segundos, más que suficiente para la calidad del aire de una sala.
Observa cómo llegan los datos
Suscríbete desde un terminal:
mosquitto_sub -h broker.connectmq.com -p 1883 -i iaq-view-3hv9 -t "connectmq/demo/iaq-3hv9/#" -v
Deberías ver mensajes como:
connectmq/demo/iaq-3hv9/office-201/air {"co2_ppm":742,"temp_c":23.4,"rh":41.8,"pm1":3,"pm25":6,"pm10":7,"pm_valid":true,"rssi":-58}
Respira suavemente cerca del SCD41 y el valor de CO2 subirá en menos de un minuto. Enciende una cerilla cerca y apágala soplando, y el PM2.5 se disparará. Ambas son comprobaciones rápidas de que los sensores y el analizador funcionan. El servidor MQTT público gratuito es ideal para esta fase de pruebas porque no necesitas cuenta, pero no es para datos de producción.
Pasar a un broker privado con TLS
Los datos del aire de una sala revelan cuándo hay personas presentes, así que los monitores de producción deben usar un broker privado. Crea un broker en el portal de ConnectMQ y cambia el código de conexión:
#include <WiFiClientSecure.h>
WiFiClientSecure net; // sustituye a WiFiClient
const int PORT = 8883;
// en setup(), antes de mqtt.setServer():
// Carga el certificado ISRG Root X1 (Let's Encrypt) para una validación completa:
// net.setCACert(ISRG_ROOT_X1_PEM);
// en connectMqtt():
// mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD");
Usa el client ID exactamente como se emitió, porque ConnectMQ vincula los client ID a las credenciales. Nuestro tutorial de ESP32 con MQTT explica con más detalle la gestión de certificados en el ESP32.
Alertas de ventilación
Configura en el portal de ConnectMQ reglas de alerta por umbral sobre los campos co2_ppm y pm25. Puntos de partida sugeridos, que debes adaptar a las recomendaciones locales y al uso de la sala:
| Nivel | CO2 | PM2.5 | Acción sugerida |
|---|---|---|---|
| Bueno | por debajo de 800 ppm | por debajo de 12 | No hay que hacer nada |
| Moderado | de 800 a 1.200 ppm | de 12 a 35 | Aumentar la ventilación, revisar el filtro |
| Malo | por encima de 1.200 ppm | por encima de 35 | Abrir ventanas o reforzar la ventilación, localizar la fuente de partículas |
Las reglas se disparan con cada valor que cruza el umbral, y el registro de alertas conserva el historial para que veas qué salas suelen estar cargadas a determinadas horas. Para accionar un ventilador o una compuerta automáticamente, suscríbete al topic del aire desde un flujo de Node-RED o una automatización de Home Assistant y conmuta el actuador mediante su propio topic de órdenes MQTT. La integración MQTT de Home Assistant hace que el monitor aparezca como entidades de sensor con unas pocas líneas de YAML.
Interpretar los datos a lo largo del tiempo
Las lecturas aisladas son menos informativas que los patrones diarios. En una sala de reuniones típica, el CO2 sube de forma constante durante cada reunión y vuelve a bajar cuando la sala se vacía; la velocidad a la que baja indica lo bien ventilada que está. Una sala que sigue por encima de 1.000 ppm una hora después de que todos se hayan ido tiene un problema de ventilación, no de ocupación. El PM2.5, en cambio, suele mantenerse bajo en interiores y muestra picos breves por cocinar, imprimir, limpiar o por sucesos exteriores como las horas punta de tráfico o el humo.
Para conservar este histórico, ejecuta un suscriptor que escriba cada mensaje en una base de datos de series temporales, o deja que Telegraf se suscriba a los topics del aire. Compara las salas entre sí y fíjate en la peor hora de cada día en lugar de en la media, porque las medias ocultan los momentos en que la gente percibe realmente el aire viciado. Compartir un sencillo gráfico semanal con el equipo de mantenimiento suele bastar para conseguir que se cambie un horario de ventilación.
Si tienes varios monitores, nombra cada topic según la sala y no según el dispositivo, de modo que una placa sustituida publique en el mismo lugar y el histórico se mantenga continuo.
Consejos de calibración y precisión
- Autocalibración del CO2. El SCD41 tiene activada por defecto la autocalibración automática, que asume que el sensor ve con regularidad aire fresco con niveles exteriores. En salas que nunca se ventilan por completo, desactívala y realiza en su lugar una recalibración forzada en el exterior.
- Calentamiento. Ignora los primeros minutos de lecturas tras el encendido.
- Autocalentamiento. Mantén el ESP32 y cualquier regulador de tensión lejos del SCD41, o la lectura de temperatura saldrá alta.
- Vida útil del sensor de partículas. El ventilador y el láser del PMS5003 se desgastan. Para despliegues largos, ponlo en reposo entre lecturas mediante su pin SET.
- Humedad y PM. Los sensores ópticos de partículas sobrestiman en aire muy húmedo porque las gotas de agua dispersan la luz. Toma con precaución las lecturas de PM por encima de un 80 por ciento de humedad relativa, aproximadamente.
Escalar a todo un edificio
Un monitor por sala de reuniones o zona diáfana es una buena densidad. Cada dispositivo necesita sus propias credenciales y un topic dentro de la jerarquía del edificio, por ejemplo acme/ist01/hq/f03/meeting-301/air. Un piloto de una docena de monitores más un panel cabe en las 25 conexiones del plan Serverless gratuito. Combinar la calidad del aire con sensores de ocupación de oficinas y monitorización HVAC muestra si el aire viciado se debe a demasiadas personas o a una ventilación insuficiente. En edificios inteligentes, de una oficina en Bogotá a un campus en Barcelona, esos mismos topics pueden colorear cada sala en un gemelo digital 3D del edificio. Un broker MQTT gestionado elimina la necesidad de ejecutar cualquier servidor en el edificio, y la guía de automatización IoT enumera más proyectos que puedes construir sobre el mismo broker.
Empieza con ConnectMQ
Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito. Graba el sketch con tus credenciales, abre el explorador de topics en vivo y configura tu primera alerta de CO2. ConnectMQ se encarga de TLS, la autenticación y la disponibilidad.
Preguntas frecuentes
¿Qué nivel de CO2 indica que una sala necesita más ventilación?
Muchas recomendaciones de ventilación consideran que unos valores sostenidos por encima de 1.000 ppm aproximadamente indican un aporte de aire fresco insuficiente para la ocupación. Sigue las recomendaciones locales y observa las tendencias en lugar de lecturas aisladas.
¿Puede un sensor de COV sustituir a un sensor de CO2 real?
No. Los sensores que informan de "CO2 equivalente" lo estiman a partir de los COV y pueden desviarse mucho. Usa un sensor NDIR o fotoacústico como el SCD41 para medir el CO2 real.
¿Con qué frecuencia debe publicar el monitor de calidad del aire interior?
Cada 30 a 60 segundos es suficiente. La calidad del aire cambia a lo largo de minutos, y publicar con menos frecuencia ahorra energía y tráfico en el broker.
¿Necesita 5 V el PMS5003?
Su ventilador necesita una alimentación de 5 V, pero su salida serie usa lógica de 3,3 V, así que el pin TX puede conectarse directamente a una entrada del ESP32.



