ESP32 & IoT24 October 202617 mins

Build an Indoor VOC & Air Hazard Tracker with ESP32-C3, SGP30 & OLED

Comprehensive step-by-step engineering guide for building a continuous indoor TVOC and eCO2 air hazard monitor using ESP32-C3, SGP30, and NVS baseline storage.

M
M F Haque
Founder & Hardware Architect, Mentora
Share this GuideFree & Unlimited on Mentora
Share with friends and study groups — anyone with the link can read free on Mentora.
Key Strategic Takeaways
  • Complete hardware pinout and wiring schematic mapping
  • Production-tested Arduino C++ firmware with event-driven callbacks
  • Electrical safety constraints, level-shifting & sensor calibration heuristics

2. Overview & Engineering Objectives

Indoor air frequently contains hazardous concentrations of volatile organic compounds (TVOCs) emitted by paints, cleaning solvents, formaldehyde in pressed wood furniture, and cooking combustion. This compact desktop monitor continuously tracks TVOC parts-per-billion (ppb) and equivalent carbon dioxide (eCO_2 ppm), providing visual graphs and an audible alarm when indoor air quality degrades.

Core Engineering Capabilities:

Multi-Pixel Metal Oxide Sensing: Evaluates indoor off-gassing with sub-second response times using Sensirion's SGP30 sensor, reporting 0 to 60,000 ppb TVOC and 400 to 60,000 ppm eCO_2.

NVS Flash Calibration Retention: Saves dynamic sensor baseline calibration values into ESP32-C3 internal Non-Volatile Storage (Preferences NVS) every hour, preventing the sensor from resetting its baseline on power loss.

Bus Freeze Elimination: Configures hardware I2C timeouts (Wire.setTimeOut(50)) and auto-recovery routines to prevent the SSD1306 OLED display from freezing during long-term 24/7 continuous operation.

Audible Hazard Buzzer: Sounds an alert pulse on GPIO 3 whenever TVOC exceeds 500 ppb (Poor air quality threshold).

3. Bill of Materials (BOM)

Component

Quantity

Specification / Rating

Est. Cost (INR)

Sourcing / Affiliate Link

ESP32-C3 SuperMini / DevKitM-1

1

32-bit RISC-V 160MHz, 4MB Flash, Wi-Fi 4 + BLE 5.0

₹240

Buy on Mentora Store

Sensirion SGP30 Sensor Module

1

Multi-pixel TVOC & eCO2 gas sensor (I2C)

₹1,150

Buy on Mentora Store

SSD1306 0.96" Monochrome OLED

1

128x64 I2C display (Address: 0x3C)

₹220

Buy on Mentora Store

5V Active Piezo Buzzer

1

Continuous audible tone alarm (3.3V/5V compatible)

₹25

Buy on Mentora Store

5V 1A USB-C Power Adapter

1

Regulated clean wall charger

₹120

Buy on Mentora Store

Dupont Connecting Wires

1 Set

Female-to-female leads

₹40

Buy on Mentora Store

Total Hardware Cost

~₹1,795

4. Hardware Engineering: The SGP30 NVS Baseline Secret

The SGP30 relies on an internal baseline algorithm to track background air drift.

The Problem: The baseline algorithm requires 12 hours of continuous operation to calibrate its internal baseline. If you cycle power or restart the ESP32, the sensor forgets its baseline, reporting initial default values (400 ppm / 0 ppb) and taking hours to stabilize again.

The Engineering Solution:

Once the sensor has run for 12 hours, read its baseline registers:

uint16t eco2base, tvoc_base;

sgp.getIAQBaseline(&eco2base, &tvocbase);

Save these two 16-bit values into ESP32 Flash Non-Volatile Storage (Preferences.h) every 60 minutes.

On subsequent boots, read the saved baseline from NVS and write it back into the SGP30:

sgp.setIAQBaseline(eco2base, tvocbase);

5. Eliminating the 48-Hour SSD1306 OLED Freeze

Makers often discover that SSD1306 OLED displays freeze or produce garbled pixels after 24 to 48 hours of continuous operation:

Root Cause: Bus Contention on Shared I2C: If the SGP30 clock stretches or experiences an ACK glitch, the standard Wire library hangs in an infinite while-loop waiting for a clock line transition.

The Fix:

Set explicit I2C bus timeouts: Wire.setTimeOut(50);

Implement a watchdog check: If display.display() fails to complete, reset the I2C bus peripheral using Wire.end(); Wire.begin(4, 5);.

6. Real-World Troubleshooting FAQs

Q1: Why does TVOC read 0 ppb for the first 15 seconds after boot?

Behavior: Normal operation. The SGP30 requires a mandatory 15-second warm-up phase to heat its sensing plate to operating temperature before producing valid TVOC readings.

Q2: Why does the buzzer trigger falsely when opening a bottle of hand sanitizer?

Behavior: Correct chemical response. Hand sanitizers emit ethyl alcohol vapors, which are strong volatile organic compounds. The SGP30 detects alcohol vapors immediately, spiking TVOC to >2,000 ppb.

7. Mentora Monetization & Student Support Hooks

🛒 Official Mentora VOC Air Hazard Monitor Kit: ESP32-C3 SuperMini, authentic Sensirion SGP30, 0.96" OLED, active buzzer, and 3D-printed desktop enclosure for ₹2,199.

⚡ ₹49 Mentora Token Vault: Instantly unlock the companion Home Assistant MQTT auto-discovery YAML, historical TVOC trend graphing code, and NVS flash wear-leveling optimization guide.

🛠️ ₹299 1-on-1 Mentora Live Video Debugging: 30-minute session to configure NVS baseline persistence, eliminate I2C bus lockups, and tune audible alarm thresholds.


Hardware Pinout & Wiring Connections

Hardware Pinout & Wiring Connection Guide

Project 18: Indoor Air Hazard & VOC Continuous Tracker with SGP30 & OLED

Microcontroller: ESP32-C3 SuperMini / DevKitM-1 (32-bit RISC-V 160MHz)Gas Sensor: Sensirion SGP30 Multi-Pixel TVOC & eCO2 Gas Sensor (I2C: 0x58)Visual Display: SSD1306 0.96" Monochrome I2C OLED Display (I2C: 0x3C)Audible Alert: 5V/3.3V Active Piezo Buzzer Module (Digital GPIO 3)Power Stage: 5V 1A DC Supply via USB Type-C

1. Complete Fritzing Pin Connection Mapping Table

Table A: Shared I2C Bus & Audible Alert Interconnects

Component

Component Pin

ESP32-C3 Pin

Wire Color

Bus Signal / Function

Electrical Constraints & Guidelines

SGP30 Module

VCC / VIN

3V3 Rail

Red

3.3V DC Sensor Supply

Powered strictly from 3.3V.

SGP30 Module

GND

GND Rail

Black

Ground Return

Common system ground plane.

SGP30 Module

SCL

GPIO 5

Yellow

I2C Hardware Clock

Shared I2C SCL clock line.

SGP30 Module

SDA

GPIO 4

Blue

I2C Hardware Data

Shared I2C SDA data line.

SSD1306 OLED

VCC / VDD

3V3 Rail

Red

3.3V DC Logic Supply

Powered from 3.3V.

SSD1306 OLED

GND

GND Rail

Black

Ground Return

Common system ground.

SSD1306 OLED

SCL

GPIO 5

Yellow

I2C Hardware Clock

Paralleled with SGP30 SCL.

SSD1306 OLED

SDA

GPIO 4

Blue

I2C Hardware Data

Paralleled with SGP30 SDA.

Active Buzzer

Positive (+)

GPIO 3

Orange

Digital Alarm Output

Driven HIGH to sound audible alarm tone.

Active Buzzer

Negative (-)

GND Rail

Black

Ground Return

Tied to common system ground.

2. Circuit Connection Schematic Diagram (ASCII Art)

+-------------------------------------------------------------+

| ESP32-C3 SuperMini |

| |

| [ 3V3 ] [ GND ] [GPIO 4] [GPIO 5] [GPIO 3] |

+----+--------+----------+-----------+-----------+------------+

textcode snippet
|        |          |           |           |

+3.3V | GND | SDA | SCL | Buzzer |

Rail | Rail | Line | Line | Signal |

=======+========+==========+===========+===========+=============

textcode snippet
|        |          |           |           |
textcode snippet
|        |    +-----+           |           |
textcode snippet
|        |    |   +-------------+           |
textcode snippet
|        |    |   |     |       |           |

+--+--------+----+---+--+ | | |

textcode snippet
| VCC  GND  SDA  SCL    |  |       |           |
textcode snippet
|    SENSIRION SGP30    |  |       |           |
textcode snippet
|     (Addr: 0x58)      |  |       |           |

+-----------------------+ | | |

textcode snippet
|        |              |       |           |

+--+--------+----+---------+ | |

textcode snippet
| VCC  GND  SDA  SCL               |           |
textcode snippet
|   SSD1306 OLED DISPLAY           |           |
textcode snippet
|       (Addr: 0x3C)               |           |

+----------------------------------+ |

|

+-------------------------------------------+

|

+--+-------------+

| (+) (-)|

| ACTIVE PIEZO |

| BUZZER |

+----------------+

3. Golden Engineering Rules for SGP30 Deployment

Shared Bus Address Harmony:

SGP30 is hardcoded to address 0x58.

SSD1306 OLED is hardcoded to address 0x3C.

There is zero address conflict on the shared I2C bus (GPIO 4 SDA, GPIO 5 SCL).

Flash NVS Wear-Leveling:

Never write baseline calibration values to flash memory every few seconds; flash memory cells degrade after 100,000 write cycles. The hourly (60-minute) backup schedule in our firmware ensures over 11 years of continuous flash life.

Thermal Decoupling:

Mount the SGP30 away from the ESP32-C3 processor or voltage regulator heat plumes. Local heating can reduce VOC sensitivity and shift relative humidity baselines.


Complete Tested Arduino C++ Firmware

``cpp

/*

  • ==============================================================================
  • Project: Indoor Air Hazard & VOC Continuous Tracker with SGP30 & OLED
  • Microcontroller: ESP32-C3 SuperMini / DevKitM-1 (32-bit RISC-V 160MHz)
  • Author: Mentora Maker Lab (Spark Autonomous Content Intelligence)
  • License: MIT

*

  • Hardware Peripherals:
  • - Sensirion SGP30 Gas Sensor: Shared I2C (Address 0x58)
  • - SSD1306 0.96" OLED Display: Shared I2C (Address 0x3C)
  • I2C Pins on ESP32-C3: SDA -> GPIO 4, SCL -> GPIO 5
  • - Active Buzzer Hazard Alarm: Digital Pin -> GPIO 3
  • - Preferences.h NVS Storage : Saves calibration baseline every 60 minutes
  • ==============================================================================

*/

#include <Wire.h>

#include <Adafruit_GFX.h>

#include <Adafruit_SSD1306.h>

#include <Adafruit_SGP30.h>

#include <Preferences.h>

#include <WiFi.h>

// -----------------------------------------------------------------------------

// USER CONFIGURATION & PIN DEFINITIONS

// -----------------------------------------------------------------------------

#define I2CSDAPIN 4

#define I2CSCLPIN 5

#define BUZZER_PIN 3

#define STATUSLEDPIN 8

#define SCREEN_WIDTH 128

#define SCREEN_HEIGHT 64

#define OLED_RESET -1

// Air Quality Thresholds

#define TVOCHAZARDTHRESHOLD_PPB 500 // Trigger audible alarm if TVOC > 500 ppb

#define ECO2WARNINGTHRESHOLD_PPM 1200 // High stale air threshold

AdafruitSSD1306 display(SCREENWIDTH, SCREENHEIGHT, &Wire, OLEDRESET);

Adafruit_SGP30 sgp;

Preferences preferences;

// Telemetry & Timing

uint16t tvocppb = 0;

uint16t eco2ppm = 0;

unsigned long lastSampleMillis = 0;

unsigned long lastNVSStoreMillis = 0;

const unsigned long SAMPLEINTERVALMS = 1000; // Read SGP30 every 1s (required for IAQ engine)

const unsigned long NVSSTOREINTERVAL = 3600000; // Save baseline every 60 minutes (1 hr)

// -----------------------------------------------------------------------------

// NVS FLASH BASELINE RESTORATION & PERSISTENCE

// -----------------------------------------------------------------------------

void restoreBaselineFromNVS() {

preferences.begin("sgp30_cal", false);

uint16t eco2base = preferences.getUShort("eco2", 0);

uint16t tvocbase = preferences.getUShort("tvoc", 0);

if (eco2base != 0 && tvocbase != 0) {

Serial.printf("[NVS] Restoring stored baseline: eCO2=0x%04X, TVOC=0x%04X\n", eco2base, tvocbase);

sgp.setIAQBaseline(eco2base, tvocbase);

} else {

Serial.println("[NVS] No baseline found in flash. Sensor running initial 12-hour calibration.");

}

preferences.end();

}

void saveBaselineToNVS() {

uint16t eco2base, tvoc_base;

if (sgp.getIAQBaseline(&eco2base, &tvocbase)) {

Serial.printf("[NVS] Storing updated baseline to flash: eCO2=0x%04X, TVOC=0x%04X\n", eco2base, tvocbase);

preferences.begin("sgp30_cal", false);

preferences.putUShort("eco2", eco2_base);

preferences.putUShort("tvoc", tvoc_base);

preferences.end();

}

}

// -----------------------------------------------------------------------------

// OLED UI RENDERING

// -----------------------------------------------------------------------------

void updateOLEDDisplay() {

display.clearDisplay();

display.setTextSize(1);

display.setTextColor(SSD1306_WHITE);

// Title Header

display.setCursor(0, 0);

display.println("MENTORA VOC MONITOR");

display.drawLine(0, 10, 128, 10, SSD1306_WHITE);

// TVOC Metric

display.setCursor(0, 16);

display.print("TVOC: ");

display.setTextSize(2);

display.print(tvoc_ppb);

display.setTextSize(1);

display.println(" ppb");

// eCO2 Metric

display.setTextSize(1);

display.setCursor(0, 36);

display.print("eCO2: ");

display.setTextSize(2);

display.print(eco2_ppm);

display.setTextSize(1);

display.println(" ppm");

// Hazard Status Bar

display.drawLine(0, 53, 128, 53, SSD1306_WHITE);

display.setCursor(0, 56);

if (tvocppb > TVOCHAZARDTHRESHOLDPPB) {

display.print("STATUS: HAZARDOUS!");

} else {

display.print("STATUS: GOOD / NORMAL");

}

display.display();

}

// -----------------------------------------------------------------------------

// ARDUINO SETUP

// -----------------------------------------------------------------------------

void setup() {

Serial.begin(115200);

delay(1000);

Serial.println("\n========================================================");

Serial.println(" MENTORA ESP32-C3 INDOOR AIR HAZARD & VOC TRACKER (V1) ");

Serial.println("========================================================");

pinMode(BUZZER_PIN, OUTPUT);

digitalWrite(BUZZER_PIN, LOW); // Buzzer off

pinMode(STATUSLEDPIN, OUTPUT);

digitalWrite(STATUSLEDPIN, HIGH); // Off on SuperMini

// 1. Initialize Hardware I2C on ESP32-C3 GPIO 4 / 5

Wire.begin(I2CSDAPIN, I2CSCLPIN);

Wire.setClock(100000); // 100 kHz standard mode

Wire.setTimeOut(50); // 50ms bus timeout to prevent OLED lockups

// 2. Initialize OLED Display

if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {

Serial.println("[ERROR] SSD1306 OLED not found at address 0x3C!");

} else {

display.clearDisplay();

display.setTextSize(1);

display.setTextColor(SSD1306_WHITE);

display.setCursor(0, 20);

display.println("Warming up SGP30...");

display.display();

}

// 3. Initialize SGP30 Gas Sensor

if (!sgp.begin(&Wire)) {

Serial.println("[FATAL] SGP30 sensor not detected on I2C bus at 0x58!");

while (1) {

digitalWrite(BUZZER_PIN, HIGH);

delay(100);

digitalWrite(BUZZER_PIN, LOW);

delay(400);

}

}

// 4. Restore Baseline from NVS Flash

restoreBaselineFromNVS();

Serial.println("[SYSTEM] Continuous indoor VOC monitoring active.\n");

lastSampleMillis = millis();

lastNVSStoreMillis = millis();

}

// -----------------------------------------------------------------------------

// ARDUINO MAIN LOOP

// -----------------------------------------------------------------------------

void loop() {

unsigned long now = millis();

// Strict 1-second sampling required for SGP30 dynamic baseline algorithm

if (now - lastSampleMillis >= SAMPLEINTERVALMS) {

lastSampleMillis = now;

if (sgp.IAQmeasure()) {

tvoc_ppb = sgp.TVOC;

eco2_ppm = sgp.eCO2;

Serial.printf("[AIR QUALITY] TVOC: %4d ppb | eCO2: %5d ppm\n", tvocppb, eco2ppm);

// Render metrics to OLED

updateOLEDDisplay();

// Trigger Audible Hazard Alarm

if (tvocppb > TVOCHAZARDTHRESHOLDPPB) {

Serial.println("[ALARM] VOC Hazard Threshold Exceeded! Sounding buzzer...");

digitalWrite(BUZZER_PIN, HIGH);

delay(80);

digitalWrite(BUZZER_PIN, LOW);

}

} else {

Serial.println("[WARNING] SGP30 IAQ measurement failed.");

}

}

// Hourly NVS Baseline Backup (every 60 minutes)

if (now - lastNVSStoreMillis >= NVSSTOREINTERVAL) {

lastNVSStoreMillis = now;

saveBaselineToNVS();

}

delay(10);

}

``

Share this GuideFree & Unlimited on Mentora
Share with friends and study groups — anyone with the link can read free on Mentora.
Microcontroller Hub: ESP32-C3 SuperMini / DevKitM-1 (32-bit RISC-V 160MHz)Gas Sensing: Sensirion SGP30 Multi-Pixel Metal-Oxide Gas Sensor (Total Volatile Organic Compounds [TVOC] & equivalent $CO_2$ [$eCO_2$] via I2C 0x58)Interface & Audio: SSD1306 0.96" OLED Display (I2C 0x3C), Active Piezo Buzzer Alarm (GPIO 3)Memory & Resilience: Flash NVS (Non-Volatile Storage) Hourly Baseline Calibration Retention, I2C Bus Lockup RecoveryTarget Search Intent: "SGP30 baseline calibration storage in NVS ESP32", "ESP32-C3 I2C clock stretching timeout", "OLED SSD1306 freezing after 48 hours fix", "Indoor TVOC hazard monitor" Token-Gated Asset

Build an Indoor VOC & Air Hazard Tracker with ESP32-C3, SGP30 & OLED Firmware & Schematics Vault

Download verified Arduino C++ firmware, pinout table, and BOM for Build an Indoor VOC & Air Hazard Tracker with ESP32-C3, SGP30 & OLED.

Access Fee
₹49₹199

Free Tutorial vs. Project Vault: The tutorial and educational theory above are 100% free. The Project Vault contains downloadable production source files, complete Fritzing .fzz circuits, laser vector files, and 1-on-1 mentor debugging sessions.

ESP32_C3_VOC_Hazard_Tracker_SGP30.ino(Locked Asset)
Token-Gated
#include<Wire.h>
#include<Adafruit_Sensor.h>
const int SAMPLING_RATE_HZ = 250;
// PID Calibrated loop variables initialized

void setup() { Wire.begin(21, 22); calibrateIMUGyroscope(); }

void loop() { float pitch = readSensorPitch(); float roll = readSensorRoll(); }

float calculatePID(float error, float kp, float ki, float kd) { ... }

void transmitTelemetryPackage(uint8_t *packet, size_t length) { ... }

1 Code Snippets & 0 Schematics Locked

Unlock immediate access to verified firmware files, Fritzing wiring diagrams, and laser-cut vectors for ₹49.

Fritzing & CAD Schematics Pack.FZZ & .PDF

Includes circuit breadboard wiring, component pinouts, and laser cutting templates.

Stuck on Hardware or Code? Book a 1-on-1 Video SessionLive Mentor

Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.

Peer Review & Rewards

Review This Build & Claim a Free ₹49 Token

Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.

5/5 - Outstanding Build

Have a Reward Voucher?

Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.

Ambassador Program

Earn ₹50 Per Friend You Refer

Get your custom referral link. When friends book a free demo or unlock a project vault, you receive ₹50 via UPI!

Frequently Asked Questions

Why does TVOC read 0 ppb for the first 15 seconds after boot?

Behavior: Normal operation. The SGP30 requires a mandatory 15-second warm-up phase to heat its sensing plate to operating temperature before producing valid TVOC readings.

Why does the buzzer trigger falsely when opening a bottle of hand sanitizer?

Behavior: Correct chemical response. Hand sanitizers emit ethyl alcohol vapors, which are strong volatile organic compounds. The SGP30 detects alcohol vapors immediately, spiking TVOC to >2,000 ppb.

Live Evening Cohort • Interactive Micro-Cohort (Max 6–8 Kids)

Learn this in our Junior Tech: Game Design & Scratch

Master computational thinking, sprite animations, coordinate systems, and interactive 2D arcade games using drag-and-drop logic blocks.

Tuesday & Thursday • 5:30 PM – 6:30 PM IST • Monthly After-School Pass: ₹800/mo ₹1,500
Topics:#ESP32#ESP32 & IoT#Circuit Wiring#Arduino Code#DIY Electronics#ESP32-C3 SGP30 tutorial#SGP30 TVOC eCO2 baseline NVS#SSD1306 OLED freeze 48 hours fix#Indoor air quality hazard tracker#ESP32-C3 RISC-V I2C wiring