Community project

Desk Air Quality Buddy

ESP32
Photo of Desk Air Quality Buddy
Generated with AI

Gabriel Trombini Filho

Last updated October 6, 2026

Desk Air Quality Buddy is a compact desk monitor that tracks temperature, humidity, and sound levels on a bright 1.28-inch round display. Built around an ESP32 microcontroller, it combines a DHT11 temperature/humidity sensor, an INMP441 I²S microphone module, and a GC9A01 circular TFT screen to give real-time feedback about your workspace environment.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to get your air quality monitor running. The included firmware handles sensor data collection, display rendering, and alert thresholds for temperature and humidity. Power the device via USB-C and keep all sensors connected during operation for continuous environmental monitoring.

Wiring diagram

Wiring diagram for Desk Air Quality Buddy

Gather all the parts

QtyComponent
1

GC9A01 Round TFT LCD 1.28 inch 240x240

1.28 inch round IPS TFT LCD display module with GC9A01/GC9A01A driver IC. 240x240 RGB resolution, 4-wire SPI interface, and 3.3V/5V module input. Module-side labels are VCC, GND, DIN, CLK, CS, DC, RST, and BL/BLK. The display is write-only over SPI, so no MISO line is required for the LCD. Supported by Adafruit GC9A01A and TFT_eSPI libraries in Arduino-compatible firmware projects.

1

HiLetgo INMP441 I²S Microphone Module

Omnidirectional 24-bit I²S MEMS microphone module based on the TDK InvenSense INMP441 IC. Outputs digital audio over a 3-wire I²S bus (SCK/BCLK, WS/LRCLK, SD/DOUT). Supply 1.8–3.3 V; native 3.3 V operation with no level shifting required. L/R channel select pin: tie to GND for left-channel mono output or VDD for right-channel mono output. Use the ESP32 Arduino core or ESP-IDF I2S peripheral API; no separate PlatformIO library is required.

1

DHT11

Digital temperature and humidity sensor (lower accuracy than DHT22)

Assemble it in 5 steps

1. Unplug before moving wires

Unplug the USB-C cable from the ESP32-C3 Super Mini before touching the breadboard. Keep the ESP32-C3, round display, microphone, and DHT11 in place; there is no battery in this version.

  • Keep the battery, wireless receiver, and any charging boards off the breadboard for now.
  • Do not connect a LiPo battery or the wireless-charging receiver to any breadboard rail in this USB-powered version.

2. Keep the round screen connected

Keep the round screen wired as follows: VCC → 3V3 (power), GND → GND (ground), SCK/SCL → GPIO4 (screen clock), MOSI/SDA/DIN → GPIO6 (screen data), CS → GPIO7 (screen select), DC → GPIO2 (screen command/data choice), and RST → 3V3 (reset held ready). If the screen has a BL pin, connect BL → 3V3 (screen light power). The screen labels SCL and SDA are for the screen itself, not for the DHT11 sensor.

  • Make sure VCC and GND are not swapped — swapped power can damage the screen.

3. Keep the microphone connected

Keep the microphone wired as follows: VDD → 3V3 (power), GND → GND (ground), L/R → GND (chooses one microphone channel), SCK → GPIO10 (sound timing), WS → GPIO20 (sound timing), and SD → GPIO21 (sound reading).

  • Use 3V3 for the microphone power; 5V can damage it.

4. Keep the temperature sensor connected

Keep the DHT11 wired as follows: VCC → 3V3 (power), GND → GND (ground), and DATA → GPIO3 (temperature and humidity reading).

  • Make sure VCC and GND are not swapped — swapped power can damage the sensor.

5. Power it from USB-C

Check that no loose jumper wire can touch another row. Then plug a USB-C cable from a normal USB power source or computer into the ESP32-C3 Super Mini. The USB-C cable powers the board, screen, microphone, and temperature sensor; it also lets Schematik install the program when you press Deploy.

  • The round screen should show temperature, humidity, room-status text, and the microphone’s changing sound-level number.
  • Do not connect the battery, charger, or wireless receiver at the same time as this simple USB-C setup.

Review all connections

1. Connections between "round_display" and "ESP32"

Functionround_displayESP32
powerVCC3V3
groundGNDGND
spiSCKGPIO 4
spiMOSIGPIO 6
spiCSGPIO 7
digitalDCGPIO 2
powerRST3V3
powerBL3V3

2. Connections between "microphone" and "ESP32"

FunctionmicrophoneESP32
powerVDD3V3
groundGNDGND
groundL/RGND
digitalSCKGPIO 10
digitalWSGPIO 20
dataSDGPIO 21

3. Connections between "dht11" and "ESP32"

Functiondht11ESP32
powerVCC3V3
groundGNDGND
dataDATAGPIO 3

Deploy the firmware

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_GC9A01A.h>
#include <DHT.h>
#include <driver/i2s.h>

constexpr int TFT_CS = 7;
constexpr int TFT_DC = 2;
constexpr int TFT_SCK = 4;
constexpr int TFT_MOSI = 6;
constexpr int MIC_SCK = 10;
constexpr int MIC_WS = 20;
constexpr int MIC_SD = 21;
constexpr int DHT_PIN = 3;
constexpr int DHT_TYPE = DHT11;
constexpr float MIC_FULL_SCALE = 2147483647.0f;
constexpr unsigned long CLIMATE_INTERVAL_MS = 2000;
constexpr float HIGH_TEMPERATURE_C = 30.0f;
constexpr float HIGH_HUMIDITY_PERCENT = 70.0f;

Adafruit_GC9A01A display(TFT_CS, TFT_DC, -1);
DHT dht(DHT_PIN, DHT_TYPE);

int lastSoundDbfs = 999;
bool alertActive = false;
float lastTemperature = NAN;
float lastHumidity = NAN;
unsigned long lastClimateReadMs = 0;

void drawStaticUi() {
  display.fillScreen(GC9A01A_BLACK);
  display.drawRoundRect(6, 6, 228, 228, 18, GC9A01A_DARKGREY);

  display.fillRoundRect(20, 16, 200, 28, 12, GC9A01A_CYAN);
  display.setTextColor(GC9A01A_BLACK);
  display.setTextSize(2);
  display.setCursor(57, 23);
  display.print("DESKBUDDY");

  display.drawRoundRect(18, 54, 98, 40, 9, GC9A01A_DARKGREY);
  display.drawRoundRect(124, 54, 98, 40, 9, GC9A01A_DARKGREY);
  display.setTextSize(1);
  display.setTextColor(GC9A01A_LIGHTGREY);
  display.setCursor(31, 61);
  display.print("TEMP");
  display.setCursor(138, 61);
  display.print("HUMIDITY");

  display.drawRoundRect(18, 104, 204, 67, 9, GC9A01A_DARKGREY);
  display.setTextColor(GC9A01A_CYAN);
  display.setCursor(91, 111);
  display.print("ALERTS");

  display.drawRoundRect(18, 180, 204, 43, 9, GC9A01A_DARKGREY);
}

void drawSoundLevel(int dbfs) {
  display.fillRoundRect(24, 187, 192, 29, 6, GC9A01A_BLACK);
  display.setTextSize(1);
  display.setTextColor(GC9A01A_LIGHTGREY);
  display.setCursor(31, 191);
  display.print("MIC");
  display.setTextColor(dbfs > -25 ? GC9A01A_YELLOW : GC9A01A_CYAN);
  display.setCursor(31, 202);
  display.print(dbfs);
  display.print(" dBFS");
  int barWidth = map(constrain(dbfs, -80, 0), -80, 0, 0, 104);
  display.drawRoundRect(101, 198, 106, 10, 5, GC9A01A_DARKGREY);
  if (barWidth > 0) {
    uint16_t color = dbfs > -25 ? GC9A01A_RED : (dbfs > -45 ? GC9A01A_YELLOW : GC9A01A_GREEN);
    display.fillRoundRect(102, 199, barWidth, 8, 4, color);
  }
}

void drawAlertStatus(bool active, float temperature, float humidity) {
  display.fillRect(28, 127, 184, 35, GC9A01A_BLACK);
  display.setTextSize(1);
  if (!active) {
    display.setTextColor(GC9A01A_GREEN);
    display.setCursor(67, 140);
    display.print("ROOM LOOKS GOOD");
    return;
  }
  display.setTextColor(GC9A01A_YELLOW);
  display.setCursor(44, 132);
  if (temperature >= HIGH_TEMPERATURE_C && humidity >= HIGH_HUMIDITY_PERCENT) {
    display.print("WARM AND HUMID");
  } else if (temperature >= HIGH_TEMPERATURE_C) {
    display.print("ROOM IS WARM");
  } else {
    display.print("ROOM IS HUMID");
  }
  display.setTextColor(GC9A01A_LIGHTGREY);
  display.setCursor(55, 150);
  display.print("CHECK THE ROOM");
}

void drawClimate(float temperature, float humidity) {
  display.fillRect(25, 72, 190, 17, GC9A01A_BLACK);
  if (isnan(temperature) || isnan(humidity)) {
    display.setTextSize(1);
    display.setTextColor(GC9A01A_ORANGE);
    display.setCursor(66, 78);
    display.print("DHT11 WAITING");
    return;
  }
  display.setTextSize(2);
  display.setTextColor(GC9A01A_WHITE);
  display.setCursor(30, 74);
  display.print(temperature, 0);
  display.print("C");
  display.setCursor(143, 74);
  display.print(humidity, 0);
  display.print("%");
}

void updateClimateIfDue() {
  if (millis() - lastClimateReadMs < CLIMATE_INTERVAL_MS) return;
  lastClimateReadMs = millis();
  float temperature = dht.readTemperature();
  float humidity = dht.readHumidity();
  if (isnan(temperature) || isnan(humidity)) return;
  bool newAlertActive = temperature >= HIGH_TEMPERATURE_C || humidity >= HIGH_HUMIDITY_PERCENT;
  if (isnan(lastTemperature) || isnan(lastHumidity) || temperature != lastTemperature || humidity != lastHumidity) {
    drawClimate(temperature, humidity);
    lastTemperature = temperature;
    lastHumidity = humidity;
  }
  if (newAlertActive != alertActive) {
    drawAlertStatus(newAlertActive, temperature, humidity);
  }
  alertActive = newAlertActive;
}

void setupMicrophone() {
  i2s_config_t config = {};
  config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX);
  config.sample_rate = 16000;
  config.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT;
  config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;
  config.communication_format = I2S_COMM_FORMAT_I2S;
  config.dma_buf_count = 4;
  config.dma_buf_len = 128;
  i2s_driver_install(I2S_NUM_0, &config, 0, nullptr);
  i2s_pin_config_t pins = {};
  pins.bck_io_num = MIC_SCK;
  pins.ws_io_num = MIC_WS;
  pins.data_out_num = I2S_PIN_NO_CHANGE;
  pins.data_in_num = MIC_SD;
  i2s_set_pin(I2S_NUM_0, &pins);
}

int readSoundDbfs() {
  int32_t samples[128];
  size_t bytesRead = 0;
  i2s_read(I2S_NUM_0, samples, sizeof(samples), &bytesRead, portMAX_DELAY);
  int sampleCount = bytesRead / sizeof(int32_t);
  if (sampleCount == 0) return -96;

  double sumSquares = 0.0;
  for (int i = 0; i < sampleCount; i++) {
    float normalized = (float)samples[i] / MIC_FULL_SCALE;
    sumSquares += normalized * normalized;
  }
  float rms = sqrtf((float)(sumSquares / sampleCount));
  float dbfs = 20.0f * log10f(fmaxf(rms, 0.00001585f));
  return (int)roundf(fmaxf(-96.0f, fminf(0.0f, dbfs)));
}

void setup() {
  dht.begin();
  setupMicrophone();
  SPI.begin(TFT_SCK, -1, TFT_MOSI, TFT_CS);
  display.begin();
  display.setRotation(0);
  drawStaticUi();
  drawClimate(NAN, NAN);
  drawAlertStatus(false, NAN, NAN);
  lastClimateReadMs = millis() - CLIMATE_INTERVAL_MS;
  updateClimateIfDue();
  lastSoundDbfs = readSoundDbfs();
  drawSoundLevel(lastSoundDbfs);
}

void loop() {
  updateClimateIfDue();
  int soundDbfs = readSoundDbfs();
  if (soundDbfs != lastSoundDbfs) {
    drawSoundLevel(soundDbfs);
    lastSoundDbfs = soundDbfs;
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik