Top 5 ESP32 Projects for School Students (With Code & Circuit)
The ESP32 is the perfect microcontroller for school science projects — it has built-in WiFi, Bluetooth, dual-core processing, and costs under $5. These 5 projects are impressive enough to stand out at any science fair, simple enough to build in a weekend, and teach real-world electronics and programming skills.
Project 1 — Smart Attendance System with RFID
Project Description
This project uses an RFID reader (RC522) connected to an ESP32 to scan student ID cards and log attendance automatically to a Google Sheet via WiFi. When a student taps their card, their name and timestamp are recorded instantly — no paper registers needed. It is a great demonstration of IoT, cloud integration, and practical automation.
Components needed: ESP32, RC522 RFID module, RFID cards/tags, buzzer, green LED, red LED, 220Ω resistors, breadboard, jumper wires.
Circuit Description
| RC522 Pin | ESP32 Pin |
|---|---|
| SDA (SS) | GPIO 5 |
| SCK | GPIO 18 |
| MOSI | GPIO 23 |
| MISO | GPIO 19 |
| RST | GPIO 27 |
| 3.3V | 3.3V |
| GND | GND |
Connect the green LED (with 220Ω resistor) to GPIO 26 and the red LED to GPIO 25. The buzzer connects to GPIO 33.
Code
#include <SPI.h>
#include <MFRC522.h>
#include <WiFi.h>
#include <HTTPClient.h>
#define SS_PIN 5
#define RST_PIN 27
#define GREEN_LED 26
#define RED_LED 25
#define BUZZER 33
MFRC522 rfid(SS_PIN, RST_PIN);
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Google Apps Script Web App URL
const char* scriptURL = "https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec";
// Registered card UIDs and names
String knownUIDs[] = {"A1B2C3D4", "E5F60718"};
String knownNames[] = {"Alice", "Bob"};
int totalStudents = 2;
void setup() {
Serial.begin(115200);
SPI.begin();
rfid.PCD_Init();
pinMode(GREEN_LED, OUTPUT);
pinMode(RED_LED, OUTPUT);
pinMode(BUZZER, OUTPUT);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500); Serial.print(".");
}
Serial.println("\nConnected!");
}
void beep(int times) {
for (int i = 0; i < times; i++) {
digitalWrite(BUZZER, HIGH); delay(100);
digitalWrite(BUZZER, LOW); delay(100);
}
}
void logAttendance(String name, String uid) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
String url = String(scriptURL) + "?name=" + name + "&uid=" + uid;
http.begin(url);
http.GET();
http.end();
}
}
void loop() {
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return;
String uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
uid += String(rfid.uid.uidByte[i], HEX);
}
uid.toUpperCase();
Serial.println("Card UID: " + uid);
bool found = false;
for (int i = 0; i < totalStudents; i++) {
if (uid == knownUIDs[i]) {
Serial.println("Welcome, " + knownNames[i]);
digitalWrite(GREEN_LED, HIGH); beep(1);
logAttendance(knownNames[i], uid);
delay(1000);
digitalWrite(GREEN_LED, LOW);
found = true;
break;
}
}
if (!found) {
Serial.println("Unknown card!");
digitalWrite(RED_LED, HIGH); beep(3);
delay(1000);
digitalWrite(RED_LED, LOW);
}
rfid.PICC_HaltA();
}
Conclusion
This project teaches RFID communication over SPI, WiFi HTTP requests, and cloud data logging — all core IoT skills. Next step: add an OLED display to show the student's name on tap, or build a web dashboard to view attendance records live.
Project 2 — Wireless Weather Station
Project Description
Build a weather station that reads temperature, humidity, and atmospheric pressure, then displays the data on a local web page served from the ESP32 itself. Any phone or laptop on the same WiFi network can open the page and see live readings — no internet or cloud account required. Great for understanding sensors, web servers, and data presentation.
Components needed: ESP32, DHT22 sensor, BMP280 pressure sensor, 10kΩ resistor, breadboard, jumper wires.
Circuit Description
| Component | Pin | ESP32 Pin |
|---|---|---|
| DHT22 | DATA | GPIO 4 |
| DHT22 | VCC | 3.3V |
| DHT22 | GND | GND |
| BMP280 | SDA | GPIO 21 |
| BMP280 | SCL | GPIO 22 |
| BMP280 | VCC | 3.3V |
| BMP280 | GND | GND |
Place a 10kΩ pull-up resistor between the DHT22 DATA pin and 3.3V.
Code
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>
#include <Adafruit_BMP280.h>
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP280 bmp;
WebServer server(80);
void handleRoot() {
float temp = dht.readTemperature();
float humidity = dht.readHumidity();
float pressure = bmp.readPressure() / 100.0F; // hPa
String html = "<!DOCTYPE html><html><head>";
html += "<meta charset='UTF-8'>";
html += "<meta http-equiv='refresh' content='10'>"; // auto-refresh every 10s
html += "<title>Weather Station</title>";
html += "<style>body{font-family:sans-serif;text-align:center;background:#f0f4f8;}";
html += ".card{display:inline-block;margin:20px;padding:30px;background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,0.1);}";
html += "h1{color:#333;} .value{font-size:2.5em;color:#2196F3;font-weight:bold;}";
html += ".unit{font-size:1em;color:#666;}</style></head><body>";
html += "<h1>🌤 Weather Station</h1>";
html += "<div class='card'><p>Temperature</p><div class='value'>" + String(temp,1) + "</div><div class='unit'>°C</div></div>";
html += "<div class='card'><p>Humidity</p><div class='value'>" + String(humidity,1) + "</div><div class='unit'>%</div></div>";
html += "<div class='card'><p>Pressure</p><div class='value'>" + String(pressure,1) + "</div><div class='unit'>hPa</div></div>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void setup() {
Serial.begin(115200);
dht.begin();
bmp.begin(0x76);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nIP: " + WiFi.localIP().toString());
server.on("/", handleRoot);
server.begin();
}
void loop() {
server.handleClient();
}
Conclusion
You've built a fully self-hosted weather station with a live web interface — no cloud required. Next steps: add a small OLED screen for standalone display, log readings to an SD card every hour, or push data to ThingSpeak for long-term graphing.
Project 3 — Automatic Plant Watering System
Project Description
This system monitors soil moisture with a capacitive sensor and automatically activates a mini water pump when the soil gets too dry. It also sends a Telegram message to your phone whenever it waters the plant. Perfect for demonstrating analog sensors, relay control, and IoT notifications in a school project.
Components needed: ESP32, capacitive soil moisture sensor, 5V mini submersible water pump, 5V relay module, small water tube, breadboard, jumper wires.
Circuit Description
| Component | Pin | ESP32 Pin |
|---|---|---|
| Soil Sensor | AOUT | GPIO 34 (ADC) |
| Soil Sensor | VCC | 3.3V |
| Soil Sensor | GND | GND |
| Relay Module | IN | GPIO 26 |
| Relay Module | VCC | 5V (Vin) |
| Relay Module | GND | GND |
Connect the water pump to the relay's NO (Normally Open) and COM terminals. Power the pump from an external 5V source — do not power it from the ESP32's 3.3V pin.
Code
#include <WiFi.h>
#include <HTTPClient.h>
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* botToken = "YOUR_TELEGRAM_BOT_TOKEN";
const char* chatID = "YOUR_TELEGRAM_CHAT_ID";
#define SOIL_PIN 34
#define RELAY_PIN 26
// Calibrate these values for your sensor
// 4095 = bone dry, ~1500 = soaking wet (capacitive sensors vary)
#define DRY_THRESHOLD 2800
void sendTelegram(String message) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String url = "https://api.telegram.org/bot" + String(botToken) +
"/sendMessage?chat_id=" + String(chatID) +
"&text=" + message;
http.begin(url);
http.GET();
http.end();
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // pump off initially
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nConnected. IP: " + WiFi.localIP().toString());
}
void loop() {
int moisture = analogRead(SOIL_PIN);
Serial.println("Soil moisture: " + String(moisture));
if (moisture > DRY_THRESHOLD) {
Serial.println("Soil dry! Watering...");
digitalWrite(RELAY_PIN, HIGH); // turn pump ON
delay(3000); // water for 3 seconds
digitalWrite(RELAY_PIN, LOW); // turn pump OFF
sendTelegram("🌱 Plant watered! Soil moisture was " + String(moisture));
delay(60000); // wait 1 minute before checking again
}
delay(5000); // check every 5 seconds
}
Conclusion
This project combines analog sensing, relay-controlled actuators, and IoT notifications — three fundamental building blocks of home automation. Next step: add a DHT22 to also monitor ambient temperature, or build a dashboard on ThingSpeak to graph soil moisture over time.
Project 4 — Quiz Buzzer System
Project Description
Build a classroom quiz buzzer system for up to 4 teams. When a team presses their button, their buzzer sounds, their LED lights up, and a 7-segment display (or serial output) shows which team buzzed first. All other buttons are locked out until the host resets the round. A fun, practical project for electronics and programming classes.
Components needed: ESP32, 4 push buttons, 4 LEDs (different colors), 4 × 220Ω resistors, 4 × 10kΩ resistors, buzzer, breadboard, jumper wires.
Circuit Description
| Component | ESP32 Pin |
|---|---|
| Team 1 Button | GPIO 13 (INPUT_PULLUP) |
| Team 2 Button | GPIO 12 (INPUT_PULLUP) |
| Team 3 Button | GPIO 14 (INPUT_PULLUP) |
| Team 4 Button | GPIO 27 (INPUT_PULLUP) |
| Reset Button | GPIO 26 (INPUT_PULLUP) |
| Team 1 LED | GPIO 32 |
| Team 2 LED | GPIO 33 |
| Team 3 LED | GPIO 25 |
| Team 4 LED | GPIO 4 |
| Buzzer | GPIO 2 |
Use INPUT_PULLUP for all buttons — wire one side to the GPIO pin and the other to GND. Each LED needs a 220Ω resistor in series to GND.
Code
const int buttonPins[] = {13, 12, 14, 27};
const int ledPins[] = {32, 33, 25, 4};
const int resetPin = 26;
const int buzzerPin = 2;
int winner = -1;
bool locked = false;
void buzz(int times, int duration = 150) {
for (int i = 0; i < times; i++) {
digitalWrite(buzzerPin, HIGH); delay(duration);
digitalWrite(buzzerPin, LOW); delay(100);
}
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < 4; i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
pinMode(ledPins[i], OUTPUT);
}
pinMode(resetPin, INPUT_PULLUP);
pinMode(buzzerPin, OUTPUT);
Serial.println("Quiz Buzzer Ready! Press a button...");
}
void loop() {
// Check reset button
if (digitalRead(resetPin) == LOW) {
locked = false;
winner = -1;
for (int i = 0; i < 4; i++) digitalWrite(ledPins[i], LOW);
Serial.println("--- RESET: Ready for next question ---");
delay(500);
}
if (!locked) {
for (int i = 0; i < 4; i++) {
if (digitalRead(buttonPins[i]) == LOW) {
winner = i + 1;
locked = true;
digitalWrite(ledPins[i], HIGH);
buzz(2);
Serial.println(">>> TEAM " + String(winner) + " BUZZED FIRST!");
delay(300); // debounce
break;
}
}
}
}
Conclusion
This project teaches digital input handling, debouncing, and state locking logic. Next step: add a 7-segment display to show the team number visually, or connect it to WiFi to display the winner on a browser scoreboard.
Project 5 — Air Quality Monitor
Project Description
This air quality monitor uses an MQ-135 gas sensor to detect harmful gases (CO2, ammonia, benzene, smoke) and a DHT22 for temperature and humidity. Readings are shown on a 0.96" OLED display and an alarm triggers when air quality drops below a safe threshold. An excellent project for environmental science topics.
Components needed: ESP32, MQ-135 gas sensor, DHT22, 0.96" I2C OLED display (SSD1306), buzzer, red LED, 220Ω resistor, breadboard, jumper wires.
Circuit Description
| Component | Pin | ESP32 Pin |
|---|---|---|
| MQ-135 | AOUT | GPIO 34 |
| MQ-135 | VCC | 5V (Vin) |
| MQ-135 | GND | GND |
| DHT22 | DATA | GPIO 4 |
| OLED | SDA | GPIO 21 |
| OLED | SCL | GPIO 22 |
| Buzzer | + | GPIO 26 |
| Red LED | + | GPIO 27 |
Code
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
#define MQ135_PIN 34
#define BUZZER_PIN 26
#define LED_PIN 27
// Above this raw ADC value = poor air quality (calibrate for your sensor)
#define AIR_THRESHOLD 2000
void setup() {
Serial.begin(115200);
dht.begin();
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(10, 25);
display.println("Air Quality Monitor");
display.display();
delay(2000);
}
void loop() {
int airValue = analogRead(MQ135_PIN);
float temp = dht.readTemperature();
float hum = dht.readHumidity();
String status = (airValue > AIR_THRESHOLD) ? "POOR" : "GOOD";
Serial.printf("Air: %d | Temp: %.1f C | Hum: %.1f%% | Status: %s\n",
airValue, temp, hum, status.c_str());
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0); display.println("-- Air Quality --");
display.setCursor(0, 16); display.printf("Air Value: %d", airValue);
display.setCursor(0, 28); display.printf("Temp: %.1f C", temp);
display.setCursor(0, 40); display.printf("Hum: %.1f %%", hum);
display.setTextSize(2);
display.setCursor(20, 50);
display.println(status == "POOR" ? "!! POOR !!" : " GOOD ");
display.display();
if (airValue > AIR_THRESHOLD) {
digitalWrite(LED_PIN, HIGH);
digitalWrite(BUZZER_PIN, HIGH); delay(200);
digitalWrite(BUZZER_PIN, LOW);
} else {
digitalWrite(LED_PIN, LOW);
}
delay(2000);
}
Conclusion
You've built a real environmental monitoring tool — the kind of sensor array used in industrial safety systems. Next steps: push readings to a ThingSpeak dashboard over WiFi, or add a second MQ sensor (MQ-7 for carbon monoxide) for a more complete air quality profile.
0 Comments