¿Alguna vez un sensor te despertó con una alarma a las 3 de la mañana por una lectura que duró un segundo y nunca pasó nada? Esos falsos positivos son el cáncer silencioso de cualquier sistema de monitoreo: gastan tu tiempo, te hacen desconfiar y, cuando llega la alerta de verdad, ya la estás ignorando. En esta guía vas a aprender a convertir un monitor de calidad de aire que dispara alertas por una sola medición en una plataforma de detección de anomalías que decide en base al historial, usando la memoria persistente que entrega Particle Ledger. Al final vas a saber crear ledgers en la nube, escribir una función Logic en JavaScript que calcula percentiles sobre una ventana de lecturas, y conectar el resultado a Slack y a un dashboard.

El problema de fondo: una lectura no es una tendencia
El punto de partida es un monitor de calidad de aire que mide CO2, partículas de polvo y un índice general de aire, y que envía esos datos a la nube de Particle cada cinco minutos. Funciona, pero tiene un defecto de diseño: dispara la alerta apenas una lectura cruza el umbral. Un auto que pasa fumando, una cocina que se prende, o simplemente ruido del sensor bastan para gatillar un aviso que no representa nada real.
La raíz del asunto es estadística. Una sola muestra no distingue entre un evento puntual y una condición sostenida que sí es peligrosa. Lo que necesitamos es que el dispositivo "recuerde" lo que vio en la última hora y recién entonces decida. Y ahí es donde el firmware del dispositivo se queda corto: la placa no tiene memoria persistente confiable para acumular series de tiempo entre reinicios. La solución no va en el equipo, va en la nube.
La estrategia: percentil 90 sobre una ventana de tiempo
En vez de preguntarnos "¿esta lectura superó el umbral?", vamos a preguntarnos algo mucho más robusto: "¿el percentil 90 de las últimas N lecturas superó el umbral?". El percentil 90 ignora los picos aislados (que quedan en el 10% superior descartado) y solo se dispara cuando la mayoría de las mediciones recientes son altas. Es la diferencia entre reaccionar a un estornudo y reaccionar a una fiebre sostenida.
Para este proyecto la regla es: alertar cuando el CO2 supere 1000 durante las últimas 12 lecturas. Como el dispositivo publica cada cinco minutos, 12 lecturas equivalen a una hora completa de aire malo antes de molestar a nadie. Ese es el corazón de la detección de anomalías: contexto temporal en lugar de gatillos instantáneos.
Hardware: el monitor base
Este tutorial parte de un proyecto existente, así que el armado físico ya está hecho. El dispositivo es una placa Particle Boron (un microcontrolador con conectividad celular integrada, ideal para puntos sin WiFi) acompañada de sensores ambientales montados sobre una base. El Boron publica los datos crudos a la nube de Particle bajo el topic aqas-env-raw, con campos como eco2 (CO2 equivalente), aq (índice de aire) y datos de polvo.
Lo importante para esta guía es entender que toda la inteligencia nueva vive en la nube, no en el firmware. No vas a recompilar ni reflashear nada en la placa: el dispositivo sigue enviando exactamente los mismos datos que antes. Todo lo que construyas acá es lógica del lado de Particle Cloud, lo que significa que puedes iterar y corregir sin tocar el hardware desplegado en terreno.
Qué es Particle Ledger y por qué lo necesitamos
Particle Ledger es el componente que le da memoria a nuestros dispositivos. Permite guardar y analizar datos en la nube por dispositivo, por producto o por cuenta de usuario. En la práctica, es un almacén clave valor (tipo gemelo digital) que persiste entre publicaciones, justo lo que nos faltaba para acumular la serie de tiempo.
Particle ofrece tres tipos de Ledger, y conviene tener clara la diferencia antes de elegir:
- Cloud Ledger: guarda información solo en la nube, sin sincronizar al dispositivo.
- Device to Cloud Ledger: almacenamiento en el dispositivo que sube automáticamente a la nube.
- Cloud to Device Ledger: datos que se setean en la nube y bajan al dispositivo.

Para nuestro caso usamos Cloud Ledger: el análisis ocurre íntegramente en la nube y el dispositivo ni se entera. Vamos a crear dos ledgers separados, uno para la configuración de umbrales y otro para los datos de la serie de tiempo. Separarlos no es capricho: el de umbrales casi nunca cambia, mientras que el de la serie se reescribe en cada ciclo.
Paso 1: crear los Ledgers
Entra a la sección Ledger desde el menú lateral de la consola de Particle y haz clic en "Create new Ledger". El primero se llama aqas-thresholds y guarda la configuración: el umbral de CO2 y cuántas lecturas componen la ventana.

Después ve a la pestaña de instancias y crea una nueva instancia con los valores de configuración (por ejemplo co2_threshold: 1000 y no_of_readings: 12). Luego repite el proceso para crear un segundo Ledger llamado aqas-time-series, y en su instancia inicializa el arreglo vacío {"co2": []}. Ese arreglo es el que se irá llenando lectura a lectura hasta completar la ventana.

Paso 2: la función Logic que hace el trabajo pesado
Ahora viene la parte interesante. Navega a la función "Logic" desde el menú lateral y crea una función basada en eventos. Una función Logic de Particle es, básicamente, una función JavaScript que se ejecuta automáticamente cada vez que llega un evento; corre en la nube, así que no consume recursos del dispositivo. Si nunca trabajaste con Logic, vale la pena revisar la documentación de Particle antes de seguir.
Copia y pega el siguiente código y despliega la función. Lo que hace, en orden: lee el umbral y el tamaño de ventana desde el ledger de configuración, agrega la lectura nueva de eco2 al arreglo de la serie de tiempo, y solo cuando junta las 12 lecturas calcula el percentil 90. Si ese percentil supera el umbral, publica el evento aqas-alert; en cualquier caso, vacía el buffer para empezar una ventana nueva. También reemite cada lectura como aqas-time-series para poder graficarla después.
import Particle from 'particle:core';
const findPercentile = (arr, pct)=> {
// Sort the array in ascending order
const sortedArr = arr.slice().sort((a, b) => a - b);
// Calculate the index for the 90th percentile
const index = Math.ceil(sortedArr.length * pct* 0.01) - 1;
// Return the value at the calculated index
return sortedArr[index];
}
export default function process({ functionInfo, trigger, event }) {
const deviceThresholdsLedger = Particle.ledger("aqas-thresholds");
const timeSeriesLedger = Particle.ledger("aqas-time-series");
const data = JSON.parse(event.eventData);
const co2_threshold = deviceThresholdsLedger.get().data.co2_threshold;
const no_of_readings = deviceThresholdsLedger.get().data.no_of_readings;
const co2_ts = timeSeriesLedger.get().data.co2||[];
co2_ts.push(data.eco2);
if(co2_ts.length >= no_of_readings){
//calculate 90th percentile
const pctCO2 = findPercentile(co2_ts,90);
console.log("percentile value ", pctCO2);
timeSeriesLedger.set({co2: []});
if( pctCO2 >=co2_threshold ){
Particle.publish("aqas-alert", {co2: pctCO2}, { productId: event.productId });
}
}else{
timeSeriesLedger.set({co2: co2_ts});
}
Particle.publish("aqas-time-series", {co2: data.eco2, aq: data.aq, seq: co2_ts.length, deviceId: event.deviceId}, { productId: event.productId, deviceId: event.deviceId });
}
Fíjate en un detalle clave de la implementación: el cálculo del percentil ordena el arreglo de menor a mayor y toma el valor en la posición Math.ceil(longitud * 90 * 0.01) - 1. Es un percentil "nearest rank" simple, sin interpolación, perfectamente suficiente para 12 muestras y mucho más liviano que traer una librería estadística completa. Y como la ventana se reinicia (timeSeriesLedger.set con el arreglo vacío) apenas se completa, evitamos que el arreglo crezca sin control y consuma cuota del ledger.
Paso 3: ver los eventos en acción
Una vez desplegada la Logic, anda a la página de Events de la consola. Vas a ver aparecer los eventos aqas-env-raw que manda el dispositivo, los aqas-time-series que reemite la función con su número de secuencia (seq), y lo más importante: el evento aqas-alert, que solo aparece cuando el percentil 90 del CO2 cruzó el umbral. Ese evento es la señal limpia que queremos propagar, ya filtrada de falsos positivos.

Paso 4: integraciones (Slack y dashboard)
Con la alerta confiable lista, conviene llevarla a donde la veas. El tutorial original arma dos integraciones.
Notificación a Slack: se usa una integración tipo webhook. Necesitas una URL de webhook de Slack, y al configurar la integración el nombre del evento debe coincidir exactamente con el que publica tu Logic (aqas-alert). Guarda, habilita, y vuelve a la página de Events para confirmar que el webhook se dispara; deberías recibir la notificación en tu app de Slack.
Dashboard en Ubidots: para visualizar las lecturas se envían los datos del evento aqas-time-series a Ubidots mediante su integración nativa. De nuevo, el nombre del evento importa. Tras guardar y habilitar, en pocos minutos (según la frecuencia con que tu dispositivo publica) las variables empiezan a poblarse en tu cuenta de Ubidots.

Y listo: pasamos de un monitor que gritaba por cualquier cosa a un dispositivo que aprende de su propio historial y solo avisa cuando de verdad importa. Sin recompilar firmware, solo con lógica en la nube.
Variantes y mejoras
Una vez que tengas la base funcionando, hay varias formas de subirle el nivel que el tutorial original no cubre:
- Umbral adaptativo por hora del día: en vez de un
co2_thresholdfijo, guarda en el ledger de configuración un arreglo de 24 umbrales (uno por hora). Una pieza cerrada de noche tolera más CO2 que una oficina llena a mediodía; ajustar el umbral por contexto reduce aún más los falsos positivos. - Detección sobre varias variables a la vez: el código actual solo evalúa CO2, pero el dispositivo también manda
aqy polvo. Replica la lógica de percentil para esas series y dispara la alerta si cualquiera de las tres cruza su umbral, o exige que dos coincidan para confirmar un evento real. - Persistir la serie completa para análisis posterior: hoy el buffer se vacía al completar la ventana. Si quieres conservar el histórico largo para detectar tendencias semanales, suma una integración a una base de datos de series de tiempo (InfluxDB, o el mismo Ubidots con retención) en paralelo al cálculo en vivo.
- Suavizado con media móvil: además del percentil 90, puedes calcular una media móvil exponencial para distinguir un alza gradual (ventilación deficiente) de un salto abrupto (fuga puntual), y etiquetar la alerta según el tipo.
Personalización para Chile
Este proyecto usa la plataforma celular de Particle, que en Chile es de nicho, pero la parte que importa (los sensores de calidad de aire) la consigues local en MechatronicStore y funcionan igual con una placa que ya tengas, sea un ESP32, un ESP8266 o un Arduino. Las lecturas de eco2 y polvo que procesa la lógica salen exactamente de estos sensores:
- Sensor de Gas CJMCU-811 (SKU GP1-6): $16.920. Es un CCS811 que entrega CO2 equivalente (eCO2) y TVOC por I2C, justo el dato
eco2que la función Logic acumula para el percentil. El equivalente directo del sensor de CO2 del tutorial. - Sensor de Polvo DSM501A PM2.5 (SKU G-231): $9.990. Cubre la lectura de polvo y material particulado fino que el monitor original también vigila.
Si quieres una alternativa más moderna al CJMCU-811, el Sensor de Calidad de Aire SGP40 VOC (SKU GL3-5, $22.810) mide compuestos orgánicos volátiles con mejor estabilidad a largo plazo. Y si buscas algo más económico para empezar, el Sensor de Calidad de Aire MQ-135 (alrededor de $4.090) sirve para prototipar la lógica antes de invertir en sensores calibrados. La placa Particle Boron no se vende en Chile, pero como toda la detección de anomalías corre en la nube, puedes adaptar el mismo concepto publicando los datos de tu ESP32 a cualquier backend con lógica equivalente.
Recursos
- Tutorial original (inglés): How to add a long lived anomaly detection system to an air quality monitor, por Mithun Das en Particle Blog.
- Documentación Particle Ledger: Particle Ledger docs
- Documentación Particle Logic: Particle Logic docs
Versión chilena con sensores en stock local en MechatronicStore. Guía basada en el trabajo original de Mithun Das.





