Community project
Desk Air Quality Buddy
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

Gather all the parts
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"
2. Connections between "microphone" and "ESP32"
3. Connections between "dht11" and "ESP32"
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.




