You have an ESP32 sitting in a drawer. Maybe you bought it months ago with big plans, or maybe it arrived last week and you are not sure where to start. Either way, this weekend is the weekend you put it to work.
We have picked five projects that go from absolute beginner to intermediate. Each one uses components you can get from wavtron.in, each one can be finished in a single sitting, and each one teaches you something genuinely useful about IoT — not just blinking an LED and calling it a day.
What you will need for all projects:
- An ESP32 DevKitC or equivalent development board
- A micro-USB cable
- Arduino IDE (2.x) or PlatformIO installed on your computer
- A breadboard and a handful of jumper wires
- About 30 minutes of patience for your first upload
If you have never programmed an ESP32 before, install the ESP32 board package in Arduino IDE first. Go to File > Preferences, paste https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json into the Additional Board Manager URLs field, then install "esp32 by Espressif Systems" from the Board Manager. Select "ESP32 Dev Module" as your board, pick the right COM port, and you are ready.
Let us get building.
Project 1: WiFi Weather Station
Difficulty: 2/5 | Build Time: 2-3 hours | Best For: First-time IoT builders
There is something deeply satisfying about checking the temperature and humidity on a screen you wired up yourself. This project reads environmental data from a DHT22 sensor, displays it on a tiny OLED screen right next to the board, and — here is the IoT part — serves a live web dashboard you can open on any phone or laptop connected to the same WiFi network.
Components
| Component | Approx. Price |
|---|---|
| ESP32 DevKitC | 500 |
| DHT22 Temperature & Humidity Sensor | 250 |
| 0.96" SSD1306 OLED Display (I2C) | 200 |
| Breadboard + Jumper Wires | 100 |
| Total | 1,050 |
Wiring
The DHT22 has three active pins. Connect VCC to the ESP32's 3.3V, GND to GND, and the DATA pin to GPIO 4. Place a 10K ohm pull-up resistor between the DATA pin and 3.3V for reliable readings — though many DHT22 breakout boards already include one.
The OLED display uses I2C. Connect SDA to GPIO 21, SCL to GPIO 22, VCC to 3.3V, and GND to GND. That is four wires for the display, three for the sensor, and you are done with hardware.
Key Code
Install three libraries from the Arduino Library Manager: DHT sensor library by Adafruit, Adafruit SSD1306, and ESPAsyncWebServer.
Here is the core logic — reading the sensor and serving the dashboard:
#include <WiFi.h>
#include <DHT.h>
#include <Adafruit_SSD1306.h>
#include <ESPAsyncWebServer.h>
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
AsyncWebServer server(80);
float temperature = 0;
float humidity = 0;
void setup() {
dht.begin();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
WiFi.begin("YOUR_SSID", "YOUR_PASSWORD");
while (WiFi.status() != WL_CONNECTED) delay(500);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
String html = "<html><head><meta http-equiv='refresh' content='5'>";
html += "<style>body{font-family:sans-serif;text-align:center;padding:40px;}</style></head>";
html += "<body><h1>Weather Station</h1>";
html += "<p>Temperature: " + String(temperature, 1) + " °C</p>";
html += "<p>Humidity: " + String(humidity, 1) + " %</p>";
html += "</body></html>";
request->send(200, "text/html", html);
});
server.begin();
Serial.begin(115200);
Serial.println(WiFi.localIP());
}
void loop() {
temperature = dht.readTemperature();
humidity = dht.readHumidity();
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("-- Weather Station --");
display.setTextSize(2);
display.setCursor(0, 20);
display.print(temperature, 1);
display.println(" C");
display.setCursor(0, 44);
display.print(humidity, 1);
display.println(" %");
display.display();
delay(2000);
}
Upload the sketch, open the Serial Monitor to find the ESP32's IP address, and type that IP into any browser on the same network. You now have a live weather dashboard.
What You Will Learn
- Reading digital sensors with single-wire protocols
- Driving an I2C OLED display
- Running a web server on a microcontroller
- Connecting an ESP32 to your WiFi network
Take It Further
Add a BMP280 pressure sensor for barometric readings. Log data to a microSD card. Push readings to ThingSpeak or your own MQTT broker for historical charts.
Project 2: Smart Door Alert with Telegram
Difficulty: 2/5 | Build Time: 1.5-2 hours | Best For: Learning API integration
Your phone buzzes. "Front door opened at 14:32." You built that. This project uses a magnetic reed switch — the same type used in commercial alarm systems — to detect when a door opens, and sends an instant notification to your Telegram account. It is simple, practical, and teaches you how microcontrollers talk to internet APIs.
Components
| Component | Approx. Price |
|---|---|
| ESP32 DevKitC | 500 |
| Magnetic Reed Switch (door sensor) | 60 |
| Breadboard + Jumper Wires | 100 |
| Total | 660 |
Wiring
A reed switch is about as simple as wiring gets. It has two wires. Connect one to GPIO 13 and the other to GND. We will use the ESP32's internal pull-up resistor in code, so no extra components are needed.
Mount the reed switch on your door frame and the magnet on the door itself. When the door is closed, the magnet holds the switch closed (circuit complete). When the door opens, the magnet moves away and the switch opens (circuit breaks). The ESP32 detects this change.
Setting Up the Telegram Bot
- Open Telegram and search for @BotFather
- Send
/newbotand follow the prompts to name your bot - Copy the API token BotFather gives you
- Send a message to your new bot, then visit
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdatesin a browser to find your chat ID
Key Code
#include <WiFi.h>
#include <HTTPClient.h>
#define REED_PIN 13
#define BOT_TOKEN "YOUR_BOT_TOKEN"
#define CHAT_ID "YOUR_CHAT_ID"
bool lastState = HIGH;
unsigned long lastAlert = 0;
void sendTelegramMessage(String message) {
HTTPClient http;
String url = "https://api.telegram.org/bot" + String(BOT_TOKEN)
+ "/sendMessage?chat_id=" + String(CHAT_ID)
+ "&text=" + message;
http.begin(url);
int httpCode = http.GET();
if (httpCode > 0) {
Serial.println("Telegram sent: " + String(httpCode));
}
http.end();
}
void setup() {
Serial.begin(115200);
pinMode(REED_PIN, INPUT_PULLUP);
WiFi.begin("YOUR_SSID", "YOUR_PASSWORD");
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println("Connected: " + WiFi.localIP().toString());
sendTelegramMessage("Door monitor online.");
}
void loop() {
bool currentState = digitalRead(REED_PIN);
// Door just opened (pin goes HIGH when magnet moves away)
if (currentState == HIGH && lastState == LOW) {
if (millis() - lastAlert > 5000) { // debounce: 5 second cooldown
sendTelegramMessage("Door opened at " + String(millis() / 1000) + "s uptime");
lastAlert = millis();
}
}
lastState = currentState;
delay(100);
}
The millis() debounce prevents a flood of messages if the door bounces. In a production version, you would replace the uptime counter with a real timestamp from an NTP server.
What You Will Learn
- Working with digital inputs and pull-up resistors
- Detecting state changes (edge detection)
- Making HTTPS API calls from a microcontroller
- Debouncing techniques for physical switches
Take It Further
Add a second reed switch for a window. Implement an NTP time sync so messages show the actual clock time. Add a "door closed" message too. Build a simple Telegram command interface so you can text /status to your bot and get a reply.
Project 3: Automated Plant Watering System
Difficulty: 3/5 | Build Time: 3-4 hours | Best For: Learning analog sensors and relay control
This is the project that makes non-technical friends say "wait, you built that?" A soil moisture sensor checks how dry your plant's soil is. When it drops below a threshold, the ESP32 triggers a relay that powers a small water pump for a few seconds. Your plants get watered automatically, and you get to learn about analog-to-digital conversion, relay safety, and control logic.
Components
| Component | Approx. Price |
|---|---|
| ESP32 DevKitC | 500 |
| Capacitive Soil Moisture Sensor v1.2 | 120 |
| 5V Single-Channel Relay Module | 80 |
| Mini Submersible Water Pump (3-5V) | 120 |
| Silicone Tubing (1m) | 40 |
| 5V 2A Power Supply (for pump) | 150 |
| Breadboard + Jumper Wires | 100 |
| Total | 1,110 |
Important: Use a capacitive soil moisture sensor, not the cheaper resistive type. Resistive sensors corrode within weeks because they pass current through the soil. Capacitive sensors last much longer.
Wiring
Soil moisture sensor: Connect VCC to 3.3V, GND to GND, and AOUT (analog output) to GPIO 34. GPIO 34 is one of the ESP32's ADC1 pins, which gives you a reading between 0 and 4095.
Relay module: Connect the relay's VCC to the ESP32's 5V (Vin pin), GND to GND, and IN to GPIO 26. On the relay's screw terminal side, wire your external 5V power supply through the COM (common) and NO (normally open) contacts to the water pump. When the relay activates, it closes this circuit and the pump runs.
A note on safety: The relay isolates the ESP32's logic circuit from the pump's power circuit. Never power the pump directly from the ESP32's GPIO pins — they can only source about 12mA, and a pump draws 100-300mA.
Key Code
#define MOISTURE_PIN 34
#define RELAY_PIN 26
// Calibrate these values for your sensor and soil
// Dry air ~3500, fully submerged in water ~1500
#define DRY_THRESHOLD 2800 // below this = soil is wet enough
#define WET_THRESHOLD 2000 // above this = soil is dry, needs water
#define PUMP_DURATION 3000 // pump runs for 3 seconds
#define CHECK_INTERVAL 60000 // check every 60 seconds
unsigned long lastCheck = 0;
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // relay module is active-LOW; HIGH = OFF
Serial.println("Plant watering system started");
}
void loop() {
if (millis() - lastCheck < CHECK_INTERVAL) return;
lastCheck = millis();
int moisture = analogRead(MOISTURE_PIN);
Serial.printf("Soil moisture: %d\n", moisture);
// Higher reading = drier soil (for most capacitive sensors)
if (moisture > DRY_THRESHOLD) {
Serial.println("Soil is dry — watering...");
digitalWrite(RELAY_PIN, LOW); // activate relay (pump ON)
delay(PUMP_DURATION);
digitalWrite(RELAY_PIN, HIGH); // deactivate relay (pump OFF)
Serial.println("Watering complete.");
} else {
Serial.println("Soil moisture is fine. No watering needed.");
}
}
Calibration is essential. Before installing the system, note the sensor reading when the probe is in dry air, when it is in a glass of water, and when it is in soil you consider "just right." Adjust DRY_THRESHOLD accordingly.
What You Will Learn
- Analog-to-digital conversion on the ESP32 (12-bit ADC)
- Controlling a relay to switch higher-power devices
- Sensor calibration and threshold tuning
- Safe circuit isolation between logic and power
Take It Further
Add WiFi and log moisture levels to a chart over time. Add a water level sensor in your reservoir so the system warns you when it is running low. Implement multiple zones with multiple sensors and pumps. Add a manual override button or a Telegram command to water on demand.
Project 4: LoRa Water Tank Level Monitor
Difficulty: 4/5 | Build Time: 4-5 hours | Best For: Learning long-range wireless communication
WiFi is great inside your house. But what about the water tank on your rooftop, or the sump three floors down, or the tank at your farmhouse 500 meters away? That is where LoRa (Long Range) comes in. This project uses an ultrasonic sensor to measure the water level in a tank, then transmits that reading wirelessly to a receiver module inside your house — no WiFi, no SIM card, no internet required, and it works over distances of 1km or more.
You will build two units: a transmitter that sits near the tank, and a receiver that sits near you and shows the water level.
Components
| Component | Approx. Price |
|---|---|
| ESP32 DevKitC x 2 | 1,000 |
| LoRa Ra-01H Module (433MHz) x 2 | 600 |
| HC-SR04 Ultrasonic Sensor | 60 |
| 0.96" SSD1306 OLED Display (I2C) | 200 |
| Breadboard x 2 + Jumper Wires | 200 |
| Total | 2,060 |
Wiring: Transmitter Unit
Ultrasonic sensor (HC-SR04): Connect VCC to 5V, GND to GND, TRIG to GPIO 5, and ECHO to GPIO 18. The ECHO pin returns a 5V signal, but ESP32 GPIOs are 3.3V tolerant on input for short pulses. For a permanent build, add a voltage divider on the ECHO line.
LoRa Ra-01H module (SPI): Connect SCK to GPIO 18 ... actually, let us avoid conflicts. Use these pins:
| Ra-01H Pin | ESP32 GPIO |
|---|---|
| SCK | GPIO 18 |
| MISO | GPIO 19 |
| MOSI | GPIO 23 |
| NSS (CS) | GPIO 5 |
| RST | GPIO 14 |
| DIO0 | GPIO 2 |
| VCC | 3.3V |
| GND | GND |
Since GPIO 5 and 18 conflict with the ultrasonic sensor, move the ultrasonic TRIG to GPIO 12 and ECHO to GPIO 13 instead.
Wiring: Receiver Unit
The receiver has the same LoRa wiring as above, plus the OLED display on SDA = GPIO 21 and SCL = GPIO 22.
Key Code
Install the LoRa library by Sandeep Mistry from the Arduino Library Manager.
Transmitter:
#include <SPI.h>
#include <LoRa.h>
#define TRIG_PIN 12
#define ECHO_PIN 13
#define TANK_HEIGHT_CM 120 // total height of your tank in cm
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
LoRa.setPins(5, 14, 2); // NSS, RST, DIO0
if (!LoRa.begin(433E6)) {
Serial.println("LoRa init failed!");
while (1);
}
LoRa.setSpreadingFactor(10);
Serial.println("Transmitter ready");
}
float measureDistance() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH, 30000);
return (duration * 0.0343) / 2.0; // cm
}
void loop() {
float distance = measureDistance();
float waterLevel = TANK_HEIGHT_CM - distance;
int percentage = constrain((waterLevel / TANK_HEIGHT_CM) * 100, 0, 100);
String packet = String(percentage) + "," + String(waterLevel, 1);
LoRa.beginPacket();
LoRa.print(packet);
LoRa.endPacket();
Serial.printf("Sent: %d%% (%.1f cm)\n", percentage, waterLevel);
delay(10000); // send every 10 seconds
}
Receiver:
#include <SPI.h>
#include <LoRa.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
void setup() {
Serial.begin(115200);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
LoRa.setPins(5, 14, 2);
if (!LoRa.begin(433E6)) {
Serial.println("LoRa init failed!");
while (1);
}
LoRa.setSpreadingFactor(10);
Serial.println("Receiver ready");
}
void loop() {
int packetSize = LoRa.parsePacket();
if (packetSize) {
String received = "";
while (LoRa.available()) {
received += (char)LoRa.read();
}
int commaIdx = received.indexOf(',');
String pctStr = received.substring(0, commaIdx);
String levelStr = received.substring(commaIdx + 1);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Water Tank Monitor");
display.drawLine(0, 12, 128, 12, SSD1306_WHITE);
display.setTextSize(3);
display.setCursor(10, 22);
display.print(pctStr + "%");
display.setTextSize(1);
display.setCursor(0, 54);
display.print("Level: " + levelStr + " cm");
display.print(" RSSI:" + String(LoRa.packetRssi()));
display.display();
Serial.printf("Received: %s%% (%s cm) RSSI: %d\n",
pctStr.c_str(), levelStr.c_str(), LoRa.packetRssi());
}
}
What You Will Learn
- LoRa radio communication fundamentals (frequency, spreading factor, packet structure)
- SPI bus communication (LoRa module uses SPI, not I2C)
- Ultrasonic distance measurement principles
- Building a two-node wireless sensor network
- RSSI (signal strength) monitoring for link quality
Take It Further
Add a buzzer on the receiver that sounds when the tank is below 20%. Use deep sleep on the transmitter to run it on a battery for months. Add multiple transmitter nodes for different tanks, each with a unique ID prefix in the packet.
Project 5: Gesture-Controlled Servo Motor
Difficulty: 3/5 | Build Time: 2-3 hours | Best For: Learning I2C communication and motion control
Strap an accelerometer to your hand, tilt it, and watch a servo motor mirror your movement in real time. This project is pure fun, but the skills it teaches — reading a 6-axis IMU over I2C, processing sensor fusion data, and driving servo motors with PWM — are the foundation of robotics, drone flight controllers, and wearable devices.
Components
| Component | Approx. Price |
|---|---|
| ESP32 DevKitC | 500 |
| MPU6050 6-Axis Accelerometer + Gyroscope | 150 |
| MG90S Metal Gear Servo Motor | 120 |
| Breadboard + Jumper Wires | 100 |
| External 5V Power Supply (for servo) | 150 |
| Total | 1,020 |
Wiring
MPU6050 (I2C): Connect VCC to 3.3V, GND to GND, SDA to GPIO 21, and SCL to GPIO 22. The MPU6050 also works at 5V, but 3.3V is fine and matches the ESP32's logic levels.
MG90S Servo: The servo has three wires — brown (GND), red (VCC), and orange (signal). Connect the orange signal wire to GPIO 13. Connect the red and brown wires to your external 5V power supply, not to the ESP32's 5V pin. Servos draw current spikes that can brown out the ESP32 and cause resets. Make sure the external supply's GND is connected to the ESP32's GND — this common ground is essential.
Key Code
Install the Adafruit MPU6050 and ESP32Servo libraries.
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include <ESP32Servo.h>
Adafruit_MPU6050 mpu;
Servo myServo;
#define SERVO_PIN 13
// Smoothing filter
float filteredAngle = 90;
const float alpha = 0.15; // lower = smoother but slower
void setup() {
Serial.begin(115200);
if (!mpu.begin()) {
Serial.println("MPU6050 not found!");
while (1) delay(10);
}
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
myServo.attach(SERVO_PIN, 500, 2400);
myServo.write(90); // start at center
Serial.println("Gesture servo ready");
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
// Convert X-axis acceleration to tilt angle
// asin gives radians, convert to degrees
float rawAngle = asin(constrain(a.acceleration.x / 9.81, -1.0, 1.0))
* (180.0 / PI);
// Map from tilt range (-60 to +60 degrees) to servo range (0 to 180)
float servoTarget = map(rawAngle * 10, -600, 600, 0, 1800) / 10.0;
servoTarget = constrain(servoTarget, 0, 180);
// Exponential moving average filter for smooth motion
filteredAngle = alpha * servoTarget + (1.0 - alpha) * filteredAngle;
myServo.write((int)filteredAngle);
Serial.printf("Tilt: %.1f -> Servo: %.1f\n", rawAngle, filteredAngle);
delay(20); // 50Hz update rate
}
The exponential moving average filter is the key detail here. Without it, the servo jitters horribly because raw accelerometer data is noisy. The alpha value of 0.15 gives a nice balance between responsiveness and smoothness. Increase it for faster response (more jitter), decrease it for silky smooth motion (more lag).
What You Will Learn
- I2C bus communication and device addressing
- Reading accelerometer data and converting to meaningful angles
- PWM servo control with the ESP32's LEDC peripheral
- Signal filtering techniques (exponential moving average)
- Why common ground matters in multi-supply circuits
Take It Further
Add the gyroscope data for a complementary filter that gives even better angle estimates. Control two servos (pan and tilt) using both the X and Y axes. Mount a laser pointer on the servo for a gesture-controlled laser. Or mount a phone on a two-axis gimbal for a DIY stabilizer.
Getting Started
If this is your first project, start with Project 1 (Weather Station) or Project 2 (Door Alert). They use the fewest components and give you the most satisfying result-to-effort ratio.
If you are comfortable with basic Arduino programming and want a challenge, jump to Project 4 (LoRa Tank Monitor). LoRa is a genuinely useful skill, and being able to send data over a kilometre without WiFi or cellular opens up possibilities you have not thought of yet.
Here is a quick comparison to help you decide:
| Project | Cost | Difficulty | Time | Key Skill |
|---|---|---|---|---|
| WiFi Weather Station | 1,050 | 2/5 | 2-3 hrs | Web servers, I2C displays |
| Smart Door Alert | 660 | 2/5 | 1.5-2 hrs | API calls, edge detection |
| Plant Watering | 1,110 | 3/5 | 3-4 hrs | Analog sensors, relay control |
| LoRa Tank Monitor | 2,060 | 4/5 | 4-5 hrs | LoRa radio, SPI protocol |
| Gesture Servo | 1,020 | 3/5 | 2-3 hrs | I2C IMU, signal filtering |
All components mentioned in these projects are available at wavtron.in. If you are in Bengaluru, you can pick up your order and start building the same day.
Tips for Success
Read the datasheet. At least skim the pinout diagram and the electrical characteristics section. Knowing that a sensor needs 3.3V instead of 5V saves you from frying a part.
Start with Serial. Before adding displays or web servers, get your sensor printing values to the Serial Monitor. Confirm the hardware works in isolation before adding complexity.
Use descriptive pin names. #define MOISTURE_PIN 34 is infinitely better than a raw 34 scattered through your code. When you inevitably rewire something, you change one line instead of hunting through your entire sketch.
Common ground everything. Any time you have two separate power supplies (one for the ESP32, one for a motor or pump), their GND pins must be connected together. Forgetting this is the number one cause of "it works on the bench but not when I connect the motor."
Do not fear the relay. Relays look intimidating because they involve switching "real" power. But a relay module with an optocoupler fully isolates your ESP32 from the high-current side. Respect the ratings, double-check your wiring, and you will be fine.
The best IoT project is the one you actually finish. Pick one, clear your Saturday afternoon, and build something real. You will learn more in three hours of hands-on wiring and debugging than in three weeks of watching tutorial videos.
Happy building.



