Si trabajas con la ventana cerrada y a media tarde te da sueño sin razón aparente, es probable que no sea el almuerzo: es el CO2 acumulado. En una pieza cerrada con una persona adentro, la concentración pasa de los 400 ppm del aire exterior a más de 1.500 ppm en un par de horas, y sobre ese nivel la capacidad de concentración cae de forma medible. Este proyecto construye un medidor que muestra ese número en una pantalla OLED y te dice, sin ambigüedad, cuándo abrir la ventana.
Al terminar vas a tener un equipo autónomo que mide CO2 equivalente, temperatura y humedad, los muestra en una OLED de 0.91", y clasifica el aire en tres estados: OK, REGULAR y VENTILA. Además vas a entender por qué existen dos familias de sensores de CO2 completamente distintas y cuál te conviene según lo que quieras medir.
Antes de comprar: no todos los sensores de CO2 miden lo mismo
Esta es la parte que el tutorial original de SparkFun da por sabida y que en Chile importa más, porque determina qué puedes conseguir localmente.
Sensores de medición directa. El STC31 del tutorial original mide la conductividad térmica del gas; los NDIR (MH-Z19, SCD30, SCD41) miden cuánta luz infrarroja absorbe el CO2 a 4,26 µm. Ambos entregan una concentración real de dióxido de carbono. El STC31 llega hasta el 100% de concentración, lo que sirve para incubadoras, fermentación o atmósferas industriales. Son caros: el breakout de SparkFun cuesta US$124,95 y no se consigue en Chile.
Sensores de CO2 equivalente (eCO2). El CCS811, que sí está en el catálogo chileno, no mide CO2. Mide compuestos orgánicos volátiles con un elemento de óxido metálico y, a partir de ellos, estima cuánto CO2 habría en un ambiente interior donde la fuente principal de ambos es la respiración humana. Es una correlación, no una medición.
¿Por qué esa distinción importa? Porque define el alcance honesto de este proyecto: sirve perfectamente para saber cuándo ventilar una pieza, una oficina o una sala de clases, y no sirve para medir atmósferas industriales, fermentación ni concentraciones altas. Si necesitas eso último, necesitas un NDIR o un STC31 y vas a tener que importarlo.
El hardware
Tres módulos I2C colgando del mismo bus, más la placa. Nada de soldadura si usas protoboard.

| Módulo | Dirección I2C | Función |
|---|---|---|
| CCS811 (CJMCU-811) | 0x5A | eCO2 y TVOC |
| BME280 | 0x76 (a veces 0x77) | temperatura, humedad y presión |
| OLED SSD1306 128x32 | 0x3C | visualización |
Las tres van al mismo par de pines: SDA al GPIO 21 y SCL al GPIO 22 del ESP32 DevKit. Como cada módulo tiene una dirección distinta, comparten el bus sin conflicto. Alimentación: el CCS811 y la OLED trabajan a 3.3 V; el BME280 en su versión de módulo acepta 3.3 V o 5 V según la placa, pero conéctalo a 3.3 V para que sus niveles lógicos coincidan con el resto.

Un detalle que ahorra media hora de frustración: el CCS811 en su versión CJMCU-811 trae el pin WAKE que debe ir a GND para que el sensor responda por I2C. Muchos módulos ya lo llevan puenteado en la placa, pero si tu sensor no aparece al escanear el bus, ese pin es el primer sospechoso.

Por qué el BME280 no es opcional
Podría parecer un agregado, pero es lo que hace confiable la lectura. La resistencia del óxido metálico del CCS811 depende fuertemente de la humedad y la temperatura: el mismo aire, medido a 20 °C y 40% HR o a 28 °C y 70% HR, entrega valores que difieren en varios cientos de ppm.
La librería expone setEnvironmentalData(humedad, temperatura) justamente para eso. Al pasarle las lecturas reales del BME280 antes de cada medición, el sensor corrige internamente su curva. Sin esa corrección, tu medidor va a marcar "ventila" cada vez que alguien se dé una ducha.
Instalación y arranque
Instala desde el Library Manager: Adafruit CCS811, Adafruit BME280 Library, Adafruit SSD1306, Adafruit GFX Library y Adafruit Unified Sensor.
#include <Wire.h>
#include <Adafruit_CCS811.h>
#include <Adafruit_BME280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define ANCHO_OLED 128
#define ALTO_OLED 32
Adafruit_SSD1306 oled(ANCHO_OLED, ALTO_OLED, &Wire, -1);
Adafruit_CCS811 ccs;
Adafruit_BME280 bme;
// Umbrales de referencia en ppm de CO2 equivalente
const uint16_t UMBRAL_BUENO = 800;
const uint16_t UMBRAL_REGULAR = 1200;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA, SCL del ESP32 DevKit de 30 pines
if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("No encuentro la OLED en la direccion 0x3C");
while (true) delay(10);
}
if (!ccs.begin(0x5A)) {
Serial.println("No encuentro el CCS811. Revisa SDA/SCL y la alimentacion.");
while (true) delay(10);
}
if (!bme.begin(0x76)) {
Serial.println("No encuentro el BME280. Prueba con la direccion 0x77.");
while (true) delay(10);
}
// El CCS811 necesita unos 20 minutos encendido antes de dar valores utiles
while (!ccs.available()) delay(100);
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.println("Calentando sensor...");
oled.display();
}
Los while (true) delay(10) después de cada begin() fallido son a propósito. Un medidor ambiental que arranca con un sensor caído y muestra ceros es peor que uno que no arranca: el primero te miente, el segundo te avisa.
La lectura y el semáforo en pantalla
const char* calidad(uint16_t eco2) {
if (eco2 < UMBRAL_BUENO) return "OK";
if (eco2 < UMBRAL_REGULAR) return "REGULAR";
return "VENTILA";
}
void loop() {
float temperatura = bme.readTemperature();
float humedad = bme.readHumidity();
// Sin compensacion ambiental el CCS811 se desvia varios cientos de ppm
ccs.setEnvironmentalData(humedad, temperatura);
if (ccs.available() && !ccs.readData()) {
uint16_t eco2 = ccs.geteCO2();
uint16_t tvoc = ccs.getTVOC();
Serial.printf("eCO2: %u ppm | TVOC: %u ppb | %.1f C | %.0f %%HR\n",
eco2, tvoc, temperatura, humedad);
oled.clearDisplay();
oled.setTextSize(2);
oled.setCursor(0, 0);
oled.print(eco2);
oled.setTextSize(1);
oled.print(" ppm ");
oled.print(calidad(eco2));
oled.setCursor(0, 22);
oled.printf("%.1fC %.0f%%HR %uppb", temperatura, humedad, tvoc);
oled.display();
} else {
Serial.println("Lectura invalida del CCS811");
}
delay(5000);
}
Sobre readData(): la librería devuelve 0 cuando la lectura fue correcta, por eso la condición se escribe negada. Es una convención tipo errno que confunde la primera vez.
Los umbrales de 800 y 1200 ppm no son arbitrarios. El aire exterior ronda los 420 ppm; bajo 800 ppm la ventilación es claramente suficiente; entre 800 y 1200 empieza a notarse; sobre 1200 ppm los estudios de calidad de aire interior reportan caída en tareas de concentración. Ajústalos si tu caso lo pide, pero ten esa referencia como punto de partida.
Calibración: la parte que casi nadie explica
El CCS811 necesita dos períodos distintos antes de entregar números confiables:
- Burn in de 48 horas la primera vez que lo enciendes. Durante ese lapso el elemento sensor se estabiliza químicamente. Déjalo conectado y simplemente ignora lo que muestre.
- Calentamiento de unos 20 minutos cada vez que lo enciendes después. Por eso el
setupespera accs.available()antes de mostrar la primera lectura.
Además, el sensor mantiene una línea base automática que asume que en algún momento del período verá aire limpio. Para que esa suposición se cumpla, deja el equipo cerca de una ventana abierta al menos una vez al día durante la primera semana. Si lo tienes en una pieza permanentemente cerrada, la línea base se corre hacia arriba y las lecturas pierden sentido.
Variantes y mejoras
- Registro y gráficos: el ESP32 tiene WiFi, así que puedes publicar las lecturas por MQTT a un Home Assistant o a InfluxDB y mirar la curva del día. Ahí se ve claramente el peak de la tarde y a qué hora conviene ventilar.
- Alerta sonora con histéresis: agrega un buzzer que suene al cruzar los 1200 ppm y que recién se rearme al bajar de 1000. Sin esa histéresis vas a escuchar el buzzer intermitente durante media hora justo en el límite.
- Ventilación automática: un relé y un ventilador de extracción convierten el medidor en un control de lazo cerrado. Con el mismo código, cambiando la salida de la OLED por un
digitalWrite. - Salto a medición real: si el proyecto crece y necesitas ppm de verdad y no eCO2, un NDIR tipo MH-Z19B o SCD41 es el camino. Hoy no están en el catálogo chileno, así que hay que importarlos, pero el resto del montaje y del código se mantiene casi igual.
Personalización para Chile
Componentes con stock local en MechatronicStore:
- ESP32 con WiFi y Bluetooth ESP WROOM-32, Tipo C (SKU X2-10V2). $7.990 CLP
- Sensor de Gas CJMCU-811 (SKU GP1-6). $16.920 CLP
- Sensor BME280 temperatura, humedad y presión 3.3V I2C (SKU GR1-10). $8.690 CLP
- Pantalla OLED 128x32 I2C 0.91" SSD1306 (SKU D-112). $3.990 CLP
- Breadboard / Protoboard mediana 400 puntos (SKU C-301). $2.690 CLP
- Cables macho hembra 40 piezas 20cm (SKU C-412). $1.990 CLP
- Cable USB Tipo C a USB Tipo A 1mt (SKU B-101). $2.190 CLP
Total aproximado: $44.460 CLP.
Equivalencias respecto del tutorial original, dichas sin adornos:
- La SparkFun IoT RedBoard ESP32 (US$29,95) se reemplaza por el ESP32 DevKit de 30 pines ($7.990): mismo chip ESP32-WROOM, misma capacidad, un tercio del precio.
- El conector Qwiic de SparkFun es simplemente I2C con un conector JST propietario. En protoboard con cables dupont haces exactamente lo mismo, y por eso este montaje no lo necesita.
- El CO2 Sensor STC31 (US$124,95) no tiene equivalente en el catálogo chileno. El CCS811 que usamos entrega CO2 equivalente, no medición directa, y no llega a concentraciones altas. Es el sustituto honesto para monitoreo de interiores, no para el caso de uso industrial del original.
Recursos
- Tutorial original: Build Your Own High Concentration CO2 Detector
- Librería del sensor: Adafruit_CCS811
- Librería de la pantalla: Adafruit_SSD1306
- Librería del BME280: Adafruit_BME280_Library
- Gráficos para la OLED: Adafruit GFX Library
Inspirado en el tutorial de SparkFun de Christo boots with the pher y Elias The Sparkiest. Versión chilena con componentes en stock local en MechatronicStore, re enfocada a monitoreo de aire interior.










