0

Stazione di Qualità dell’Aria con MQ e Display AI.

Share

Introduzione alla Stazione di Qualità dell’Aria

Ecco uno sketch Arduino completo per una “stazione qualità aria” con MQ‑135, MQ‑2, BME280 e display, che include: riscaldamento e calibrazione dei sensori MQ (R0 in aria pulita), letture periodiche, stima gas index da MQ, conversione in AQI stimato (PM2.5-like) tramite breakpoints EPA, più visualizzazione su display.

L’AQI è una stima euristica perché MQ‑135/MQ‑2 non misurano direttamente PM2.5.

Hardware e librerieBME280 via I2C per temperatura/umidità/pressione; sostituire SEALEVELPRESSURE_HPA con il valore locale per migliore altitudine.

MQ‑135 e MQ‑2 su moduli analogici: serve riscaldamento iniziale e calibrazione in aria pulita per stimare R0; poi Rs viene ricavata da Vout e convertita in rapporto Rs/R0 per stimare concentrazione relativa.

AQI: calcolo tramite formula a tratti con breakpoints PM2.5 EPA (24h). Nel progetto si usa una media mobile/NowCast semplice per l’ora corrente per un “AQI stimato”.

Wiring tipicoBME280: VCC 3.3V, GND, SCL-A5, SDA-A4 (Arduino Uno) oppure pin I2C della vostra board.

MQ‑135 su A0, MQ‑2 su A1; usare 5V e rispettare i tempi di preriscaldamento indicati in datasheet/modulo.

Codice Arduino (commentato in italiano)Nota: usare un display I2C 128×64 (SSD1306) o 1602 I2C (modificare se diverso). Include: calibrazione R0, letture, stima gas-index, NowCast semplice e conversione in AQI stimato con breakpoints EPA.

/* Stazione Qualità Aria – MQ-135, MQ-2, BME280, Display I2C
   Funzioni:
   - Calibrazione R0 (aria pulita) per MQ-135 e MQ-2
   - Letture periodiche con preriscaldamento
   - Gas index (relativo) da Rs/R0
   - PM2.5 stimato euristico dai gas + umidità (proxy)
   - AQI stimato (EPA PM2.5) con media mobile/NowCast semplice
   Avvertenza: MQ non misurano PM2.5; AQI qui è una stima indicativa, non certificabile.
*/

#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <Adafruit_BME280.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

Adafruit_BME280 bme;

// Pin analogici per MQ
const int PIN_MQ135 = A0;
const int PIN_MQ2   = A1;

// Costanti elettriche: misurate il resistore di carico RL del vostro modulo (in ohm)
const float RL_MQ135 = 10000.0; // es. 10k, misurare sul vostro modulo
const float RL_MQ2   = 5000.0;  // es. 5k, misurare sul vostro modulo

// Calibrazione: R0 in aria pulita (verrà appreso in setup)
float R0_MQ135 = -1.0;
float R0_MQ2   = -1.0;

// Parametri ADC
#if defined(ARDUINO_ARCH_ESP32)
  const float ADC_MAX = 4095.0;
  const float VREF    = 3.3;
#else
  const float ADC_MAX = 1023.0;
  const float VREF    = 5.0;
#endif

// BME280: pressione livello mare locale (hPa) per altitudine corretta
#define SEALEVELPRESSURE_HPA (1013.25) // sostituire con valore locale meteo per accuratezza [vedi note]

// Tempi preriscaldamento/calibrazione
const unsigned long MQ_PREHEAT_MS   = 3UL * 60UL * 1000UL; // 3 min minimo moduli; meglio 24h per stabilità
const unsigned long CALIB_WINDOW_MS = 30UL * 1000UL;       // 30s media in aria pulita

// Campionamento
const unsigned long SAMPLE_MS = 2000;

// Smoothing per AQI (NowCast semplice: EMA)
float pm25_est_ema = -1.0;
const float NOWCAST_ALPHA = 0.2;

// Utility: calcola Rs dal valore ADC
float rs_from_adc(int adc, float RL) {
  float vout = (adc / ADC_MAX) * VREF;
  if (vout <= 0.001) vout = 0.001;
  float rs = RL * ((VREF / vout) - 1.0);
  return rs;
}

// Stima euristica PM2.5 (µg/m3) da MQ e BME: proxy grezza, da tarare sul campo
float estimate_pm25_from_gas(float rsr0_135, float rsr0_2, float humidity) {
  // Maggiore inquinamento → Rs/R0 più basso tipicamente per molti target gas
  // Combina due sensori e aggiusta per umidità alta che può aumentare letture apparentemente “sporche”.
  float gas_index = ( (1.0 / max(rsr0_135, 0.1)) * 0.6 + (1.0 / max(rsr0_2, 0.1)) * 0.4 );
  // Correzione umidità: riduce leggermente a RH molto alta
  float hum_factor = 1.0 - constrain((humidity - 50.0) * 0.002, 0.0, 0.2);
  float pm = gas_index * 15.0 * hum_factor; // fattore scala da tarare localmente
  return constrain(pm, 0.0, 500.0);
}

// AQI EPA PM2.5 con breakpoint e formula piecewise
int aqi_from_pm25(float pm) {
  // Breakpoints EPA PM2.5 (µg/m3) e AQI [web:11][web:12][web:14]
  struct BP { float Cl; float Ch; int Il; int Ih; };
  static const BP bps[] = {
    {0.0, 12.0,   0,  50},
    {12.1, 35.4, 51, 100},
    {35.5, 55.4,101, 150},
    {55.5,150.4,151, 200},
    {150.5,250.4,201,300},
    {250.5,350.4,301,400},
    {350.5,500.4,401,500}
  };
  if (pm > 500.4) return 500;
  for (auto &bp : bps) {
    if (pm >= bp.Cl && pm <= bp.Ch) {
      float aqi = ( (bp.Ih - bp.Il) / (bp.Ch - bp.Cl) ) * (pm - bp.Cl) + bp.Il;
      return (int)round(aqi);
    }
  }
  return 0;
}

// Calibrazione R0 in aria pulita: media su finestra
float calibrate_R0(int pin, float RL) {
  unsigned long t0 = millis();
  double acc = 0; int n = 0;
  while (millis() - t0 < CALIB_WINDOW_MS) {
    int adc = analogRead(pin);
    float rs = rs_from_adc(adc, RL);
    acc += rs;
    n++;
    delay(100);
  }
  float rs_avg = (n > 0) ? (acc / n) : RL;
  // R0 definito come Rs in aria pulita (approssimazione comune per MQ) [datasheet]
  return rs_avg;
}

void draw_screen(float tempC, float hum, float pres_hPa, float alt_m,
                 float rsr0_135, float rsr0_2, float pm25_est, int aqi) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  display.setCursor(0,0);
  display.print("T:"); display.print(tempC,1); display.print("C  ");
  display.print("RH:"); display.print(hum,0); display.print("%");

  display.setCursor(0,10);
  display.print("P:"); display.print(pres_hPa,1); display.print("hPa ");
  display.print("Alt:"); display.print(alt_m,0); display.print("m");

  display.setCursor(0,20);
  display.print("MQ135 Rs/R0: "); display.print(rsr0_135,2);

  display.setCursor(0,30);
  display.print("MQ2   Rs/R0: "); display.print(rsr0_2,2);

  display.setCursor(0,40);
  display.print("PM2.5 est: "); display.print(pm25_est,1); display.print(" ug/m3");

  display.setCursor(0,54);
  display.print("AQI stimato: "); display.print(aqi);

  display.display();
}

void setup() {
  Serial.begin(115200);
  Wire.begin();

  // Display
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    // fall back silenzioso se non presente
  } else {
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0,0);
    display.print("Stazione Qualita Aria");
    display.setCursor(0,10);
    display.print("Riscaldamento MQ...");
    display.display();
  }

  // BME280
  bool ok = bme.begin(0x76); // cambiare a 0x77 se necessario
  if (!ok) {
    Serial.println("BME280 non trovato!");
  }

  // Preriscaldamento MQ
  Serial.println("Preriscaldamento MQ in corso...");
  unsigned long t0 = millis();
  while (millis() - t0 < MQ_PREHEAT_MS) {
    delay(250);
  }

  // Calibrazione in aria pulita (portare i sensori all'esterno lontano da inquinanti)
  Serial.println("Calibrazione R0 in aria pulita (30s)...");
  if (display.width()) {
    display.clearDisplay();
    display.setCursor(0,0);
    display.print("Calibrazione R0...");
    display.display();
  }
  R0_MQ135 = calibrate_R0(PIN_MQ135, RL_MQ135);
  R0_MQ2   = calibrate_R0(PIN_MQ2,   RL_MQ2);

  Serial.print("R0_MQ135 = "); Serial.println(R0_MQ135, 1);
  Serial.print("R0_MQ2   = "); Serial.println(R0_MQ2, 1);

  pm25_est_ema = -1.0;
}

unsigned long lastSample = 0;

void loop() {
  if (millis() - lastSample < SAMPLE_MS) return;
  lastSample = millis();

  // Letture BME280
  float tempC = NAN, hum = NAN, pres_hPa = NAN, alt_m = NAN;
  if (bme.sensorID()) {
    tempC    = bme.readTemperature();
    hum      = bme.readHumidity();
    pres_hPa = bme.readPressure() / 100.0;
    alt_m    = bme.readAltitude(SEALEVELPRESSURE_HPA);
  }

  // Letture MQ
  int adc135 = analogRead(PIN_MQ135);
  int adc2   = analogRead(PIN_MQ2);

  float rs135 = rs_from_adc(adc135, RL_MQ135);
  float rs2   = rs_from_adc(adc2,   RL_MQ2);

  float rsr0_135 = (R0_MQ135 > 0) ? (rs135 / R0_MQ135) : NAN;
  float rsr0_2   = (R0_MQ2   > 0) ? (rs2   / R0_MQ2)   : NAN;

  // Stima PM2.5 grezza dai gas e umidità
  float pm25_est = estimate_pm25_from_gas(rsr0_135, rsr0_2, isfinite(hum)?hum:50.0);

  // NowCast/EMA semplice
  if (pm25_est_ema < 0) pm25_est_ema = pm25_est;
  else pm25_est_ema = NOWCAST_ALPHA * pm25_est + (1.0 - NOWCAST_ALPHA) * pm25_est_ema;

  // AQI da PM2.5 stimato
  int aqi = aqi_from_pm25(pm25_est_ema);

  // Serial log
  Serial.print("T="); Serial.print(tempC,1);
  Serial.print("C RH="); Serial.print(hum,0);
  Serial.print("% P="); Serial.print(pres_hPa,1);
  Serial.print("hPa Alt="); Serial.print(alt_m,0); Serial.print("m | ");

  Serial.print("MQ135 Rs/R0="); Serial.print(rsr0_135,2);
  Serial.print(" MQ2 Rs/R0="); Serial.print(rsr0_2,2); Serial.print(" | ");

  Serial.print("PM2.5_est="); Serial.print(pm25_est_ema,1);
  Serial.print(" ug/m3 AQI="); Serial.println(aqi);

  // Display
  draw_screen(tempC, hum, pres_hPa, alt_m, rsr0_135, rsr0_2, pm25_est_ema, aqi);
}