Why You Should Care About Air Quality (Besides Your Lungs)

Let’s be honest—most of us don’t think about air quality unless we’re coughing our way through a particularly smoggy day. But here’s the thing: air quality data is everywhere, and it’s actionable. Governments need it for policy decisions, city planners need it for urban development, and increasingly, individuals need it to decide whether today is a “stay inside” day or a “go for a run” day. The problem? Traditional air quality monitoring is expensive, requires specialized equipment, and often provides data from only a handful of fixed locations. What if I told you that you could build a sophisticated, real-time air quality forecasting system for a fraction of the cost using IoT sensors and some clever engineering? Over the next few thousand words, we’re going to build exactly that. We’ll design a system that doesn’t just measure air quality—it predicts it. And yes, there will be code. Lots of it.

Understanding the System Architecture

Before we dive into the weeds, let’s establish what we’re actually building. Our system consists of three interconnected layers: the sensing layer (hardware collecting data), the network layer (connectivity and gateways), and the analytics layer (forecasting and visualization).

graph TB A["Smart-Air Sensors
PM2.5 | PM10 | CO2
NO2 | O3 | Temperature"] -->|Wi-Fi/LTE| B["IoT Gateway
Data Aggregation
Local Processing"] B -->|HTTPS| C["Cloud Platform
Time-Series Database
ML Models"] C -->|API| D["Web Dashboard
Real-time Visualization
Forecasts"] C -->|WebSocket| E["Mobile Application
Push Notifications
Health Recommendations"] F["Historical Data
Weather Patterns
Traffic Data"] -.->|Training Data| C style A fill:#e1f5ff style B fill:#fff3e0 style C fill:#f3e5f5 style D fill:#e8f5e9 style E fill:#fce4ec

This architecture allows us to scale from monitoring a single neighborhood to an entire metropolitan area. Each component is loosely coupled, meaning you can upgrade individual parts without rebuilding the entire system.

Hardware Components: Choosing Your Sensors

The heart of any air quality monitoring system is the sensor suite. You’re not just measuring PM10 anymore—we’re going full spectrum. Here’s what we’ll integrate: Particulate Matter Sensors measure the concentration of suspended particles (PM2.5, PM10). The laser-based dust sensors like the SDS011 provide excellent accuracy at a reasonable price point. Think of it as your system’s eyes for spotting the tiny troublemakers in the air. Gas Sensors are where things get interesting. The MQ135 sensor detects CO2, benzene, NH3, and NOx—basically everything that makes your respiratory system file a complaint. It’s not laboratory-grade precision, but it’s surprisingly reliable for environmental monitoring. Environmental Sensors measure temperature, humidity, and barometric pressure. Why? Because air quality doesn’t exist in a vacuum (literally—air does exist in a vacuum, but you know what I mean). These parameters influence pollutant behavior and are crucial for accurate forecasting. For the microcontroller, we’ll use NodeMCU (ESP8266) or Arduino MKR WiFi 1010. NodeMCU offers an excellent balance of connectivity, processing power, and cost. It’s WiFi-enabled, has GPIO pins for sensor interfacing, and comes with sufficient memory for local data buffering.

Setting Up Your Development Environment

Before any sensors see electrons, let’s prepare our development environment. I’m assuming you have some familiarity with Arduino IDE, but if you don’t, the learning curve is gentle—think “IKEA instructions, but actually clear.” First, install the Arduino IDE if you haven’t already. Add the NodeMCU board support:

  1. Open Arduino IDE and navigate to File > Preferences
  2. In “Additional Boards Manager URLs,” add: http://arduino.esp8266.com/stable/package_esp8266com_index.json
  3. Go to Tools > Board > Boards Manager and search for “ESP8266”
  4. Install the latest version Now, we need libraries for our sensors. Go to Sketch > Include Library > Manage Libraries and install:
  • MQ135 by bestfitme (for gas sensor)
  • SDS011 dust sensor library
  • DHT sensor library by Adafruit (for temperature/humidity)
  • ArduinoJson by Benoit Blanchon (for JSON handling)

Building the Sensor Node: Hardware Assembly

Physical assembly time. Here’s where theory meets a breadboard and occasionally frustration:

NodeMCU Pin Connections:
- D1 (GPIO5)    → DHT22 Data Pin
- D2 (GPIO4)    → MQ135 Analog Out → A0
- D4 (GPIO2)    → SDS011 RX Pin
- D3 (GPIO0)    → SDS011 TX Pin
- GND          → Common Ground (DHT, MQ135, SDS011)
- 3.3V         → DHT22 VCC, MQ135 VCC, SDS011 VCC
- Vin          → External 5V Power Supply

Why Vin instead of the USB connection for power? Because sensors are power-hungry little parasites, and USB can’t provide consistent voltage under load. Trust me on this—I learned through charred resistors and tears.

The Firmware: Where Magic Happens

Now for the code. This is the firmware that runs on your NodeMCU, collecting data and communicating with the cloud:

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <SoftwareSerial.h>
// WiFi Configuration
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
const char* cloudServer = "your-cloud-endpoint.com";
// Sensor Configuration
#define DHT_PIN D1
#define MQ135_PIN A0
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
SoftwareSerial sdsSerial(D3, D4); // RX, TX for SDS011
// Global Variables
float temperature = 0;
float humidity = 0;
float pm25 = 0;
float pm10 = 0;
int mq135Value = 0;
unsigned long lastTransmission = 0;
const unsigned long TRANSMISSION_INTERVAL = 300000; // 5 minutes
// Structure for sensor readings
struct SensorReading {
  float temperature;
  float humidity;
  float pm25;
  float pm10;
  int mq135;
  unsigned long timestamp;
};
void setup() {
  Serial.begin(115200);
  delay(100);
  // Initialize sensors
  dht.begin();
  sdsSerial.begin(9600);
  Serial.println("\n\nSystem Initializing...");
  // WiFi Connection
  connectToWiFi();
}
void loop() {
  // Check WiFi connection
  if (WiFi.status() != WL_CONNECTED) {
    connectToWiFi();
  }
  // Read sensors
  readDHTSensor();
  readMQ135Sensor();
  readSDS011Sensor();
  // Transmit data if interval elapsed
  if (millis() - lastTransmission > TRANSMISSION_INTERVAL) {
    transmitData();
    lastTransmission = millis();
  }
  delay(2000);
}
void readDHTSensor() {
  float h = dht.readHumidity();
  float t = dht.readTemperature();
  if (!isnan(h) && !isnan(t)) {
    humidity = h;
    temperature = t;
    Serial.printf("DHT22 - Temp: %.1f°C, Humidity: %.1f%%\n", temperature, humidity);
  }
}
void readMQ135Sensor() {
  int rawValue = analogRead(MQ135_PIN);
  mq135Value = rawValue;
  // Convert to PPM (simplified; calibration needed for production)
  float ppm = (rawValue / 1024.0) * 500;
  Serial.printf("MQ135 - Raw: %d, Estimated CO2: %.0f ppm\n", rawValue, ppm);
}
void readSDS011Sensor() {
  // SDS011 Protocol: Read 10 bytes starting with 0xAA
  if (sdsSerial.available() >= 10) {
    if (sdsSerial.read() == 0xAA) {
      byte data;
      sdsSerial.readBytes(data, 8);
      byte checksum = sdsSerial.read();
      if (data == 0xAB) {
        pm25 = (data + data * 256) / 10.0;
        pm10 = (data + data * 256) / 10.0;
        Serial.printf("SDS011 - PM2.5: %.1f µg/m³, PM10: %.1f µg/m³\n", pm25, pm10);
      }
    }
  }
}
void transmitData() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi not connected. Skipping transmission.");
    return;
  }
  HTTPClient http;
  String url = String("http://") + cloudServer + "/api/readings";
  // Create JSON payload
  StaticJsonDocument<256> doc;
  doc["device_id"] = "SENSOR_001";
  doc["temperature"] = temperature;
  doc["humidity"] = humidity;
  doc["pm25"] = pm25;
  doc["pm10"] = pm10;
  doc["mq135"] = mq135Value;
  doc["timestamp"] = millis();
  String jsonString;
  serializeJson(doc, jsonString);
  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  int httpResponseCode = http.POST(jsonString);
  if (httpResponseCode > 0) {
    Serial.printf("HTTP Response Code: %d\n", httpResponseCode);
    String response = http.getString();
    Serial.println("Response: " + response);
  } else {
    Serial.printf("Error: %s\n", http.errorToString(httpResponseCode).c_str());
  }
  http.end();
}
void connectToWiFi() {
  Serial.print("Connecting to WiFi: ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nWiFi Connected!");
    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("\nFailed to connect to WiFi");
  }
}

This firmware does several important things. First, it reads all three sensor types at regular intervals. Second, it handles WiFi connectivity with retry logic—because networks are flaky. Third, it packages the data as JSON and transmits it to your cloud endpoint every five minutes. The key design decision here is the transmission interval. Five minutes is a sweet spot: frequent enough to capture meaningful changes, infrequent enough to avoid overwhelming your cloud infrastructure.

The Cloud Layer: Data Ingestion and Storage

Your sensors are now happily transmitting data to the cloud. Now we need infrastructure to receive, store, and process it. Here’s a simple Node.js backend using Express and InfluxDB:

const express = require('express');
const { InfluxDB, Point } = require('@influxdata/influxdb-client');
const app = express();
app.use(express.json());
// InfluxDB Configuration
const influxDB = new InfluxDB({
  url: 'http://localhost:8086',
  token: 'your-influxdb-token',
  org: 'your-organization',
  bucket: 'air-quality'
});
const writeApi = influxDB.getWriteApi('your-organization', 'air-quality');
const queryApi = influxDB.getQueryApi('your-organization');
// Receive sensor data
app.post('/api/readings', async (req, res) => {
  try {
    const { device_id, temperature, humidity, pm25, pm10, mq135, timestamp } = req.body;
    // Validate data
    if (!device_id || temperature === undefined || pm25 === undefined) {
      return res.status(400).json({ error: 'Missing required fields' });
    }
    // Create data points for InfluxDB
    const point1 = new Point('temperature')
      .tag('device', device_id)
      .floatField('value', temperature)
      .timestamp(new Date(timestamp));
    const point2 = new Point('pm25')
      .tag('device', device_id)
      .floatField('value', pm25)
      .timestamp(new Date(timestamp));
    const point3 = new Point('pm10')
      .tag('device', device_id)
      .floatField('value', pm10)
      .timestamp(new Date(timestamp));
    const point4 = new Point('humidity')
      .tag('device', device_id)
      .floatField('value', humidity)
      .timestamp(new Date(timestamp));
    // Write to InfluxDB
    writeApi.writePoints([point1, point2, point3, point4]);
    await writeApi.flush();
    res.status(200).json({ 
      status: 'success',
      message: 'Data received and stored'
    });
  } catch (error) {
    console.error('Error processing reading:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});
// Query latest readings for dashboard
app.get('/api/latest/:device_id', async (req, res) => {
  try {
    const { device_id } = req.params;
    const fluxQuery = `
      from(bucket: "air-quality")
        |> range(start: -1h)
|--|--|
        |> last()

    `;
    const results = [];
    await queryApi.queryRows(fluxQuery, {
      next(row, tableMeta) {
        const o = tableMeta.toObject(row);
        results.push({
          measurement: o._measurement,
          value: o._value,
          time: o._time
        });
      },
      error(error) {
        console.error('Query error:', error);
      }
    });
    res.status(200).json(results);
  } catch (error) {
    console.error('Error querying data:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Why InfluxDB? Because it’s purpose-built for time-series data. Storing IoT sensor readings in a relational database is like using a hammer to screw in a nail—technically possible, but deeply unsatisfying. InfluxDB handles data retention policies, automatic downsampling, and time-range queries with ridiculous efficiency.

Forecasting: The Intelligent Part

Now here’s where we shift from “monitoring” to “forecasting.” Real-time data is great, but predictive insights? That’s where the magic happens. We’re going to use historical data patterns combined with external factors to predict air quality in the next 24 hours.

const tf = require('@tensorflow/tfjs-node');
const SMA = require('simple-moving-average');
class AirQualityForecaster {
  constructor() {
    this.model = null;
    this.lookbackWindow = 24; // Use 24 hours of historical data
    this.forecastHorizon = 24; // Predict 24 hours ahead
  }
  // Prepare training data from InfluxDB
  async prepareTrainingData(deviceId, days = 30) {
    const fluxQuery = `
      from(bucket: "air-quality")
        |> range(start: -${days}d)
|--|--|
        |> sort(columns: ["_time"])

    `;
    const measurements = [];
    await queryApi.queryRows(fluxQuery, {
      next(row, tableMeta) {
        measurements.push(tableMeta.toObject(row)._value);
      }
    });
    return this.createTrainingSequences(measurements);
  }
  // Create sequences for LSTM training
  createTrainingSequences(data) {
    const X = [];
    const y = [];
    for (let i = 0; i < data.length - this.lookbackWindow - this.forecastHorizon; i++) {
      X.push(data.slice(i, i + this.lookbackWindow));
      y.push(data.slice(
        i + this.lookbackWindow, 
        i + this.lookbackWindow + this.forecastHorizon
      ));
    }
    return { X, y };
  }
  // Build LSTM model
  buildModel() {
    this.model = tf.sequential({
      layers: [
        tf.layers.lstm({
          inputShape: [this.lookbackWindow, 1],
          units: 32,
          returnSequences: true
        }),
        tf.layers.dropout({ rate: 0.2 }),
        tf.layers.lstm({
          units: 16,
          returnSequences: false
        }),
        tf.layers.dropout({ rate: 0.2 }),
        tf.layers.dense({ units: this.forecastHorizon })
      ]
    });
    this.model.compile({
      optimizer: 'adam',
      loss: 'meanSquaredError'
    });
  }
  // Train the model
  async train(X, y, epochs = 50) {
    const xTensor = tf.tensor3d(X);
    const yTensor = tf.tensor2d(y);
    await this.model.fit(xTensor, yTensor, {
      epochs: epochs,
      batchSize: 32,
      validationSplit: 0.2,
      verbose: 1
    });
    xTensor.dispose();
    yTensor.dispose();
  }
  // Make predictions
  async forecast(recentData) {
    // Normalize data to 0-1 range
    const min = Math.min(...recentData);
    const max = Math.max(...recentData);
    const normalized = recentData.map(v => (v - min) / (max - min));
    const input = tf.tensor3d([normalized]);
    const prediction = this.model.predict(input);
    // Denormalize predictions
    const predictions = prediction.dataSync();
    const denormalized = Array.from(predictions).map(v => v * (max - min) + min);
    input.dispose();
    prediction.dispose();
    return denormalized;
  }
}
// Usage
const forecaster = new AirQualityForecaster();
forecaster.buildModel();
// Train on historical data
const trainingData = await forecaster.prepareTrainingData('SENSOR_001', 30);
await forecaster.train(trainingData.X, trainingData.y);
// Make predictions
const recentReadings = [35, 38, 42, 40, 45, 50, 48, 52, 55, 60, 62, 65, 63, 68, 70, 72, 75, 78, 80, 82, 85, 88, 90, 92];
const forecast = await forecaster.forecast(recentReadings);
console.log('Next 24 hours PM2.5 forecast:', forecast);

The forecasting model uses LSTM (Long Short-Term Memory) neural networks, which are excellent for time-series prediction. LSTM networks understand temporal dependencies—they recognize that air quality at 3 PM is influenced by conditions at 2 PM, 1 PM, and so on. The training process uses 30 days of historical data to identify patterns. Traffic typically increases during rush hours, affecting pollution. Weather patterns repeat seasonally. Industrial activity follows schedules. Your LSTM model learns all of this.

Building the Dashboard: Visualization Layer

What’s a forecasting system without a beautiful interface? Here’s a Vue.js component for real-time visualization:

<template>
  <div class="dashboard">
    <div class="metrics-grid">
      <div class="metric-card">
        <h3>Current PM2.5</h3>
        <div :class="['value', getPMClass(currentPM25)]">
          {{ currentPM25 }}
        </div>
        <p>µg/</p>
      </div>
      <div class="metric-card">
        <h3>Temperature</h3>
        <div class="value">{{ temperature }}°C</div>
      </div>
      <div class="metric-card">
        <h3>Humidity</h3>
        <div class="value">{{ humidity }}%</div>
      </div>
    </div>
    <div class="charts">
      <canvas id="forecastChart"></canvas>
    </div>
    <div class="alerts">
      <div v-if="highPollutionAlert" class="alert alert-danger">
        ⚠️ High pollution levels expected. Consider staying indoors.
      </div>
      <div v-if="moderatePollutionAlert" class="alert alert-warning">
         Moderate pollution levels. Sensitive groups should limit outdoor activity.
      </div>
    </div>
  </div>
</template>
<script>
import axios from 'axios';
import Chart from 'chart.js/auto';
export default {
  data() {
    return {
      currentPM25: 0,
      temperature: 0,
      humidity: 0,
      forecast: [],
      chart: null,
      highPollutionAlert: false,
      moderatePollutionAlert: false
    };
  },
  mounted() {
    this.fetchLatestData();
    this.fetchForecast();
    setInterval(() => this.fetchLatestData(), 30000); // Update every 30 seconds
    setInterval(() => this.fetchForecast(), 3600000); // Update forecast hourly
  },
  methods: {
    async fetchLatestData() {
      try {
        const response = await axios.get('/api/latest/SENSOR_001');
        const data = response.data.reduce((acc, item) => {
          acc[item.measurement] = item.value;
          return acc;
        }, {});
        this.currentPM25 = data.pm25 || 0;
        this.temperature = data.temperature || 0;
        this.humidity = data.humidity || 0;
        this.evaluateAlerts();
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    },
    async fetchForecast() {
      try {
        const response = await axios.get('/api/forecast/SENSOR_001');
        this.forecast = response.data;
        this.updateChart();
      } catch (error) {
        console.error('Error fetching forecast:', error);
      }
    },
    evaluateAlerts() {
      this.highPollutionAlert = this.currentPM25 > 150;
      this.moderatePollutionAlert = this.currentPM25 > 50 && this.currentPM25 <= 150;
    },
    getPMClass(value) {
      if (value <= 50) return 'good';
      if (value <= 100) return 'moderate';
      if (value <= 150) return 'unhealthy-sensitive';
      return 'unhealthy';
    },
    updateChart() {
      if (this.chart) {
        this.chart.destroy();
      }
      const ctx = document.getElementById('forecastChart').getContext('2d');
      this.chart = new Chart(ctx, {
        type: 'line',
        data: {
          labels: Array.from({ length: 24 }, (_, i) => `${i}:00`),
          datasets: [{
            label: 'PM2.5 Forecast',
            data: this.forecast,
            borderColor: '#ff6b6b',
            backgroundColor: 'rgba(255, 107, 107, 0.1)',
            tension: 0.4,
            fill: true
          }]
        },
        options: {
          responsive: true,
          plugins: {
            legend: {
              display: true,
              position: 'top'
            }
          },
          scales: {
            y: {
              beginAtZero: true,
              title: {
                display: true,
                text: 'PM2.5 (µg/m³)'
              }
            }
          }
        }
      });
    }
  }
};
</script>
<style scoped>
.dashboard {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}
.metrics-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin-bottom: 40px;
}
.metric-card {
  background: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.metric-card h3 {
  margin: 0 0 10px 0;
  color: #666;
  font-size: 14px;
}
.value {
  font-size: 32px;
  font-weight: bold;
  margin-bottom: 5px;
}
.value.good { color: #27ae60; }
.value.moderate { color: #f39c12; }
.value.unhealthy-sensitive { color: #e74c3c; }
.value.unhealthy { color: #c0392b; }
.alerts {
  margin-top: 30px;
}
.alert {
  padding: 15px;
  border-radius: 4px;
  margin-bottom: 10px;
}
.alert-danger {
  background-color: #fadbd8;
  color: #78281f;
}
.alert-warning {
  background-color: #fdeaa8;
  color: #7d6608;
}
</style>

This dashboard provides real-time data visualization and alert systems. The color-coded PM2.5 values follow AQI standards, making it immediately clear whether air quality is good, moderate, or hazardous.

Scaling Considerations: From One Sensor to a Network

What we’ve built so far handles a single sensor node. But what if you want to monitor an entire city? Here’s where architecture matters. Horizontal Scaling: Add more sensor nodes. Each transmits data independently to your cloud backend. Use device IDs to distinguish them. Data Aggregation: Your backend should support querying across multiple devices. Create spatial analysis—identify pollution hotspots by correlating readings from nearby sensors. Edge Processing: For processing-intensive tasks (like ML inference), push computation to edge gateways. This reduces cloud load and improves response times. Data Retention: Use time-series database retention policies to archive old data. You don’t need granular data from a year ago—downsample it to daily averages.

Calibration and Accuracy: The Uncomfortable Truth

Here’s the thing nobody wants to hear: cheap sensors drift. The MQ135 today isn’t the MQ135 from three months ago. This is why professional air quality monitoring requires periodic calibration against reference standards. Implement multi-point calibration:

async calibrateAgainstReference(deviceId, referenceValue, measurement) {
  // Collect 100 readings from your sensor
  const readings = await collectReadings(deviceId, 100);
  const average = readings.reduce((a, b) => a + b) / readings.length;
  // Calculate offset
  const offset = referenceValue - average;
  // Store calibration data
  const calibration = {
    device_id: deviceId,
    measurement: measurement,
    offset: offset,
    timestamp: new Date(),
    reference_value: referenceValue
  };
  await db.saveCalibration(calibration);
  return offset;
}

Apply these offsets to incoming sensor data. Do this quarterly for accuracy-critical applications.

Deployment and Monitoring: The Never-Ending Story

Your system is built. Now it needs to stay built. Implement comprehensive monitoring:

  • Sensor Health Checks: Detect sensors that have stopped transmitting or are returning nonsensical values
  • Data Quality Metrics: Track the percentage of missing data, outliers, and transmission failures
  • System Performance: Monitor API response times, database query speeds, and cloud resource utilization
  • Alert Systems: Get notified when something breaks at 3 AM so you can fix it before breakfast

Looking Ahead: Advanced Enhancements

Your base system works. But there’s always more:

  • Integrate Weather APIs: Correlate air quality with wind patterns, precipitation, and atmospheric pressure
  • Add Traffic Data: Use real-time traffic information to improve pollution forecasts
  • Machine Learning Refinement: Retrain your LSTM model weekly with new data, continually improving accuracy
  • Mobile App Push Notifications: Alert users when pollution levels are expected to spike
  • Integration with Smart City Platforms: Connect with traffic management systems to influence routing during high pollution periods

Conclusion

You’ve now built a sophisticated air quality forecasting system from scratch. You understand how sensors work, how to process data efficiently, how to forecast future conditions, and how to present information meaningfully. The beautiful part? You can start small—literally with one NodeMCU and a handful of sensors on your desk—and scale to a city-wide network. Each component is modular, replaceable, and independently scalable. Is it perfect? No. Will you encounter edge cases and weird sensor behavior? Absolutely. Will you occasionally stay up at night debugging why your humidity reading is 250% (it happens)? Definitely. But you’ll have built something that contributes to public health, enables better policy decisions, and demonstrates that sophisticated IoT systems don’t require million-dollar budgets or PhDs in atmospheric science. Just persistence, some caffeine, and a willingness to debug at 2 AM. Now go forth and measure some air quality. The world’s lungs will thank you.