๐ก A Note for Newcomers: Don't panic! This tutorial is written specifically for first-time ESP32 users. Every step is explained in detail โ just follow along and you'll succeed. If you run into trouble, jump straight to the Troubleshooting section near the end โ almost every issue has an answer there.
By completing this project, you'll have a smart ESP32 device that you can control from your phone:
Simple Analogy: Think of it like using your phone to control your home AC โ your phone is the remote, the ESP32 is the AC, and WiFi is the "telephone line" connecting them.
Gather the following items (check them off as you go):
| # | Component Name | Quantity | Purpose | โ Done |
|---|---|---|---|---|
| 1 | ESP32 Development Board | 1 pc | Main controller | โ |
| 2 | DHT11 Temperature & Humidity Sensor | 1 pc | Measure temp & humidity | โ |
| 3 | 0.96" OLED Display (128ร64, I2C) | 1 pc | Show time, temp & humidity | โ |
| 4 | Passive Buzzer (3.3V-5V, PWM driven) | 1 pc | Alarm ringing (requires PWM square wave) | โ |
| 5 | DC Motor | 1 set | Remote motor control | โ |
| 6 | Push Button (tactile switch) | 1 pc | Manually stop alarm when ringing | โ |
| 7 | Breadboard | 1 pc | For easy wiring | โ |
| 8 | Jumper Wires (M-M, M-F) | Several | Connect everything together | โ |
| 9 | Micro USB / Type-C Data Cable (NOT charge-only!) | 1 pc | Connect PC to flash code | โ |
โ ๏ธ CRITICAL SAFETY WARNING: Always DISCONNECT the ESP32 from USB power before wiring! Check your connections 3 times before applying power. Wrong wiring can damage your components!
| ESP32 Pin | Connects To | Notes |
|---|---|---|
| GPIO4 | DHT11 DATA / OUT | Temperature/humidity data pin |
| GPIO21 | OLED SDA | I2C Data Line |
| GPIO22 | OLED SCL | I2C Clock Line |
| GPIO18 | Passive Buzzer Signal Pin (IN / SIG) | PWM-driven buzzer ringing |
| GPIO19 | Motor Driver Module IN1 / Signal Input | Control motor on/off |
| GPIO5 | Push Button (other terminal) | Manually stop alarm when ringing |
| GPIO2 (Built-in) | (Already on the board, no wiring needed) | Onboard LED |
Open the libraries folder in the project folder, then load it following the library loading process.
Internet_of_Things_WIFI_Control_System_ESP32) to a location you can easily find (like your Desktop)Internet_of_Things_WIFI_Control_System_ESP32.ino file inside that folder โ it will automatically open in Arduino IDE.ino, NTPTime.cpp, OLEDDisplay.cpp, etc. โ this is the "multi-file modular" structure. Each file handles one specific job (we'll explain this in detail later).โ ๏ธ If you skip this step, WiFi connection WILL fail later!
In the tabs at the top of the IDE window, click to switch to the WebServer.cpp file
Find this section of code (around line 10):
// STA mode configuration: Connects to your router to get NTP time
const char* sta_ssid = "YourWiFiName"; // โ ๏ธ Change to YOUR WiFi name!
const char* sta_password = "YourWiFiPassword"; // โ ๏ธ Change to YOUR WiFi password!
Replace the text inside the double quotes with your own home WiFi credentials:
sta_ssid: Change to your WiFi name (SSID)sta_password: Change to your WiFi passwordExample:
const char* sta_ssid = "MyHomeNetwork";
const char* sta_password = "mypassword123";
(Optional) If you want a custom hotspot name and password, modify these too:
const char* ap_ssid = "ESP32_AP"; // Name shown in phone WiFi list
const char* ap_password = "12345678"; // Hotspot password โ at least 8 chars
After successful flashing, let's use the Serial Monitor to see what the ESP32 is doing:
Ctrl+Shift+M=== Getting NTP time at startup ===
Connecting to WiFi: MyHomeNetwork
.....
โ
WiFi connected successfully!
NTP sync attempt 1/30: โ
Success!
Current time: 15:30:45
๐ถ Starting AP hotspot: ESP32_AP
AP IP address: 192.168.4.1
Time: 15:30:46
T = 25.5 deg. C, H = 60.0%
Time: 15:30:47
...
โ
WiFi connected successfully! โ WiFi connected โNTP sync attempt 1/30: โ
Success! โ Time synchronized โAP IP address: 192.168.4.1 โ Hotspot created successfully โ=== Getting NTP time at startup ===
Connecting to WiFi: WrongSSID
........................................
โ WiFi connection failed, unable to sync NTP time
๐ถ Starting AP hotspot: ESP32_AP
AP IP address: 192.168.4.1
Time: 00:00:01
Time: 00:00:02
...
00:00:00 (like a digital watch that hasn't been set)After successful OLED initialization, the screen should display:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ALARM 08:30 โ Shows here ONLY if alarm is set
โ 08:30 45 โ Large Hours:Minutes + Seconds
โ T:25.5C H:60.0% โ Temperature and Humidity
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
When sensor fails: If the DHT11 reading fails (wiring issue or sensor fault), the bottom row will show ERR instead of values, for example:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 08:30 45
โ T:ERRC H:ERR% โ Sensor reading abnormal
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
If the screen is blank, go to Troubleshooting Q3 / Q4 below.
ESP32_AP (or your custom hotspot name) and tap to connect12345678 (or your custom password)192.168.4.1 in the address bar and press Enter / GoA: This is the #1 beginner mistake โ almost 100% of the time it's the wrong baud rate.
A: Check each of these systematically, in order:
WebServer.cpp and double-check that sta_ssid and sta_password are correctA: OLED failed to initialize. Check each item:
0x3C, but a few use 0x3DOLEDDisplay.cpp, find the oledInit() function0x3C to 0x3D in the display.begin(...) callA: This is almost always a contrast/brightness issue. In OLEDDisplay.cpp, in the oledInit() function, add these two lines AFTER display.begin(...):
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(0xCF); // 0x00 = dimmest, 0xFF = brightest. Adjust to taste.
A: Check each item:
A:
A:
192.168.4.1 againESP32_AP โ Modify Network โ Static IP settings:
192.168.4.2192.168.4.1255.255.255.0A:
Request: GET /H HTTP/1.1A:
tone() to output 1000Hz square wave) โ DC voltage alone won't make it sound โ โ
This project is designed for theseALARM XX:XX appear at the top of the OLED?โฐ Alarm set: XX:XX?A:
Motor state updated: ON| Symptom | Most Likely Cause | Fastest Fix |
|---|---|---|
| Serial garbage / blank | Wrong baud rate | Set to 115200, press EN to reset |
| WiFi won't connect (many dots) | Wrong password / 5GHz WiFi | Verify credentials, use 2.4GHz |
| OLED blank + error message | Wrong wiring / wrong I2C address | Check I2C wiring, try address 0x3D |
| OLED blank + NO error | Contrast too low | Add contrast adjustment code |
| OLED shows T:ERR / H:ERR | DHT11 wiring issue / sensor fault | Check GPIO4 wiring and 4.7kฮฉ pull-up resistor |
| DHT11 always 0.0 | Wrong wiring / reversed pin order | Check GPIO4, verify pinout on silk screen |
| Can't find hotspot | Code not flashed properly | Re-flash code, press EN to reset |
| Can't open web page | Phone auto-switched WiFi | Disable auto-switch, try incognito mode |
| Buttons unresponsive | Browser cache | Refresh page / restart ESP32 |
| Alarm silent | Wrong wiring / wrong buzzer type | Check GPIO18, use passive buzzer (PWM driven) |
| Motor won't move | No driver / no external power | Add motor driver, connect motor power supply |
| Physical button can't stop alarm | Button wiring / no pull-up | Check GPIO5 wiring, button's other terminal to GND |
๐ก Congratulations if you've already gotten the project working! This section is for those who want to understand the "why" behind the code. First-timers can safely skip this and just enjoy the feeling of success.
IoT (Internet of Things) is all about connecting devices together through a network so they can communicate and be controlled remotely.
This project is a classic three-layer IoT application:
The project code is split into 9 files, each responsible for one specific thing. This is called multi-file modular programming.
| File Name | What It Does | Real-World Analogy |
|---|---|---|
Internet_of_Things_WIFI_Control_System_ESP32.ino |
Main program, the boss | Company CEO |
NTPTime.h / .cpp |
Connects WiFi, fetches network time | Admin Dept โ keeps the clock accurate |
DHTSensor.h / .cpp |
Reads DHT11 temp & humidity | Environmental monitor |
OLEDDisplay.h / .cpp |
Controls OLED screen display | Receptionist โ shows info to visitors |
WebServer.h / .cpp |
Creates hotspot, handles web requests | Customer Service Dept |
Module Relationship Diagram:
Main Program (.ino) โโ calls โ NTPTime Module (get time)
โโ DHTSensor Module (read temp & humidity)
โโ OLEDDisplay Module (display info)
โโ WebServer Module (create hotspot + web page)
Why Use Modular Programming?
| Advantage | Explanation |
|---|---|
| Easy to Debug | If temp/humidity is wrong, just look in DHTSensor.cpp โ no need to hunt through 1000 lines |
| Easy to Reuse | Need OLED in your next project? Just copy OLEDDisplay.h/.cpp straight over! |
| Clear Logic | Each file does one thing โ fewer chances of bugs |
Boot-Up Sequence Used in This Project:
Why switch modes? While ESP32 technically supports simultaneous STA+AP (hybrid mode), in practice it causes many resource conflicts and poor stability. Sequential switching ("STA for time โ disconnect โ start AP") gives far higher reliability.
millis() Instead of delay()?This is a very important concept in embedded programming โ non-blocking programming.
โ The Problem with delay(1000): It "freezes" the entire program for 1 full second. During that frozen time, it can't ring alarms, can't handle web requests, can't read buttons.
โ
The Beauty of millis(): millis() returns "how many milliseconds have elapsed since boot". We use this to check if "enough time has passed" โ if not, we move on and do other things.
// Correct approach: Non-blocking
if (millis() - lastTime >= 1000) {
lastTime = millis();
// 1 second is up โ execute the timed task
}
// Code below here keeps running every single loop โ never gets stuck!
๐ About the
statickeyword: Astaticvariable declared inside a function is NOT destroyed when the function exits โ the next time the function is called, it still remembers its old value. Think of it as "memory" inside a function โ perfect for remembering when we last did something.
๐
tone()/noTone()โ PWM Drive for Passive Buzzer: This project uses a passive buzzer which requires a PWM square wave to produce sound.tone(pin, 1000)generates a 1000Hz square wave on the pin, andnoTone(pin)stops it. Compared to manually flipping pin levels withmillis(),tone()is implemented by hardware timers and doesn't consume any CPU time โ much more efficient.
๐ Button Detection โ Falling Edge Trigger: The physical button uses "falling edge detection" โ it only triggers once at the exact moment the button transitions from released (HIGH) to pressed (LOW), rather than continuously triggering while held down. This is implemented using a
staticvariable to remember the previous button state.
#include <WiFi.h> // ESP32 WiFi library (NOT WiFiS3.h!)
#include <Wire.h> // I2C communication (for OLED)
#include "OLEDDisplay.h" // OLED display module
#include "WebServer.h" // Web server module
#include "NTPTime.h" // NTP time module
#include "DHTSensor.h" // Temp/humidity sensor module
// ==================== Buzzer (passive, PWM driven) ====================
#define BUZZER_PIN 18 // Buzzer โ GPIO18
#define BUZZER_FREQ 1000 // 1000Hz PWM frequency (passive buzzer needs square wave)
// ==================== Buzzer Stop Button ====================
#define BUZZER_BTN_PIN 5 // Button โ GPIO5
// ==================== Motor Control ====================
#define MOTOR_PIN 19 // Motor โ GPIO19
// ==================== LED ====================
#define LED_PIN 2 // Onboard LED โ GPIO2
bool motorState = false; // Motor on/off state variable
// Global variables
unsigned long currentUnixTime = 0; // Current timestamp (seconds)
unsigned long lastUpdateTime = 0; // Last update time (milliseconds)
// Alarm variables
int alarmHour = 0;
int alarmMin = 0;
bool alarmOn = false; // Whether alarm is enabled
bool alarmRing = false; // Whether alarm is currently ringing
// Temperature & Humidity variables
float temperature = 0.0;
float humidity = 0.0;
setup(): Initialization Function (Runs ONCE at boot)void setup() {
Serial.begin(115200); // Initialize serial at 115200 baud
delay(1000);
pinMode(LED_PIN, OUTPUT); // Set LED pin as output
pinMode(MOTOR_PIN, OUTPUT); // Set motor pin as output
digitalWrite(LED_PIN, LOW); // Initial state: LED OFF
digitalWrite(MOTOR_PIN, LOW); // Initial state: motor OFF
// Buzzer stop button (active low with internal pull-up)
pinMode(BUZZER_BTN_PIN, INPUT_PULLUP);
// Wait for DHT11 sensor to stabilize
Serial.print("[DHT] Sensor type: DHT11, Pin: GPIO");
Serial.println(DHT_SENSOR_PIN);
Serial.println("[DHT] Waiting for sensor stabilization...");
delay(2000);
Wire.begin(21, 22); // I2C init: SDA=GPIO21, SCL=GPIO22
oledInit(); // Initialize OLED display
// ===== Boot-up Sequence =====
getNetTime(currentUnixTime); // Step 1: Connect WiFi, get NTP time
startAPMode(); // Step 2: Switch to AP mode, create hotspot
server.begin(); // Step 3: Start Web server
lastUpdateTime = millis(); // Step 4: Record timekeeping reference point
}
loop(): Main Loop (Runs REPEATEDLY forever)The main loop contains 4 core tasks, each implemented in a non-blocking way:
Core 1: Poll DHT sensor + per-second timekeeping + display update
// Poll DHT sensor every loop iteration (module internally handles 3s non-blocking sampling)
measureEnvironment(&temperature, &humidity);
if (millis() - lastUpdateTime >= 1000) {
lastUpdateTime = millis();
currentUnixTime++; // Increment timestamp +1 second
// Split timestamp into hours, minutes, seconds (using modulo arithmetic)
int h = (currentUnixTime % 86400L) / 3600; // 86400 = seconds per day
int m = (currentUnixTime % 3600) / 60; // 3600 = seconds per hour
int s = currentUnixTime % 60;
// Print time (zero-padded for neat display)
Serial.print("Time: ");
if(h<10) Serial.print("0"); Serial.print(h);
Serial.print(":");
if(m<10) Serial.print("0"); Serial.print(m);
Serial.print(":");
if(s<10) Serial.print("0"); Serial.println(s);
// Update OLED display
updateOLED(h, m, s, alarmHour, alarmMin, alarmOn, temperature, humidity);
// Alarm trigger check (fires ONLY at exactly the 0th second of target minute)
if(alarmOn && h == alarmHour && m == alarmMin && s == 0){
alarmRing = true;
}
}
Core 2: Handle web requests (checked EVERY loop iteration)
handleWebRequest(currentUnixTime, alarmHour, alarmMin, alarmOn, alarmRing, BUZZER_PIN, temperature, humidity, motorState);
Core 3: Alarm ringing (PWM-driven passive buzzer + button stop)
// Buzzer button: press to stop alarm
static bool lastBtnState = HIGH;
bool btnState = digitalRead(BUZZER_BTN_PIN);
if (btnState == LOW && lastBtnState == HIGH) {
delay(20); // simple debounce
btnState = digitalRead(BUZZER_BTN_PIN);
if (btnState == LOW) {
if (alarmRing) {
alarmRing = false;
alarmOn = false;
noTone(BUZZER_PIN);
Serial.println("Button pressed: alarm stopped");
}
}
}
lastBtnState = btnState;
// Alarm ringing (PWM square wave drives passive buzzer)
if (alarmRing) {
tone(BUZZER_PIN, BUZZER_FREQ); // Output 1000Hz PWM square wave
} else {
noTone(BUZZER_PIN); // Stop PWM output
}
Core 4: Motor state change detection (only write to pin when state actually changes)
static bool lastMotorState = false;
if(motorState != lastMotorState) {
lastMotorState = motorState;
digitalWrite(MOTOR_PIN, motorState ? HIGH : LOW);
Serial.print("Motor state updated: ");
Serial.println(motorState ? "ON" : "OFF");
}
| Function | Purpose |
|---|---|
WiFi.mode(WIFI_STA) |
Switch to STA mode (preparing to connect to router) |
WiFi.begin(ssid, pass) |
Begin connecting to specified WiFi |
WiFi.status() |
Check connection status (WL_CONNECTED = success) |
WiFi.disconnect(true) |
Disconnect current WiFi connection |
WiFi.mode(WIFI_OFF) |
Turn off WiFi module completely |
WiFi.softAP(ssid, pass) |
Create AP hotspot (ESP32 specific! UNO R4 uses beginAP()) |
WiFi.softAPIP() |
Get ESP32's own IP in AP mode (default 192.168.4.1) |
| Function | Purpose |
|---|---|
tone(pin, frequency) |
Output PWM square wave on specified pin. frequency sets the wave frequency in Hz, e.g. tone(18, 1000) outputs 1000Hz to drive passive buzzer |
noTone(pin) |
Stop PWM square wave output โ turns off tone() output, stops passive buzzer completely |
millis() |
Returns milliseconds since boot โ used for non-blocking timing: millis() - lastTime >= 1000 checks if 1 second has passed |
When you click buttons on the web page, your browser sends these HTTP requests. The ESP32 parses them and takes the corresponding action:
| Request Path | Function | ESP32 Action |
|---|---|---|
GET /H |
Turn LED ON | digitalWrite(LED_BUILTIN, HIGH) |
GET /L |
Turn LED OFF | digitalWrite(LED_BUILTIN, LOW) |
GET /MOTOR_ON |
Turn Motor ON | motorState = true |
GET /MOTOR_OFF |
Turn Motor OFF | motorState = false |
GET /ALARM?h=08&m=30 |
Set Alarm | Parse h and m, set alarm time, alarmRing = false |
GET /ALARM_OFF |
Turn Off Alarm | alarmRing = false; alarmOn = false; alarmHour=0; alarmMin=0; noTone(BUZZER_PIN); |
Ready to customize? Start here!
Open WebServer.cpp:
const char* sta_ssid = "YourHomeWiFi";
const char* sta_password = "YourHomePassword";
const char* ap_ssid = "MyESP32"; // Custom hotspot name
const char* ap_password = "MySecurePass88"; // Hotspot password (min 8 chars)
Open NTPTime.cpp:
const char* ntpServer = "pool.ntp.org"; // Global NTP pool
// const char* ntpServer = "time.google.com"; // Google time server
// const char* ntpServer = "ntp.jst.mfeed.ad.jp"; // Japan NTP server
const long gmtOffset_sec = 0 * 3600; // UTC (London)
// const long gmtOffset_sec = 9 * 3600; // Tokyo (UTC+9)
// const long gmtOffset_sec = -5 * 3600; // New York (UTC-5)
| What to Change | Where to Change |
|---|---|
| DHT11 data pin | DHTSensor.h โ #define DHT_SENSOR_PIN |
| Buzzer pin | Main .ino file โ #define BUZZER_PIN |
| Motor pin | Main .ino file โ #define MOTOR_PIN |
| LED pin | Main .ino file โ #define LED_PIN |
| I2C pins | Main .ino file โ setup() โ Wire.begin(SDA, SCL) |
Add this block at the END of the main program's loop() function (AFTER Core 4):
// ====== Core 5: Auto temperature fan control (check every 5 seconds) ======
static unsigned long lastAutoCtrlTime = 0;
if(millis() - lastAutoCtrlTime >= 5000) {
lastAutoCtrlTime = millis();
if(temperature > 30.0 && !motorState) {
motorState = true;
Serial.println("๐ก๏ธ Temperature > 30ยฐC, auto fan ON");
} else if(temperature <= 28.0 && motorState) {
motorState = false;
Serial.println("๐ก๏ธ Temperature <= 28ยฐC, auto fan OFF");
}
}
Some ESP32 pins are connected to onboard SPI Flash or have special boot functions โ using them as regular GPIO will cause crashes:
| Pins | Why NOT to Use | Recommendation |
|---|---|---|
| GPIO6 ~ GPIO11 | Connected to onboard SPI Flash | ๐ด ABSOLUTELY DO NOT USE! Will crash / brick your board |
| GPIO0 | Boot/download pin โ must be HIGH at power-on | Avoid using as output |
| GPIO2 | Must be LOW during firmware download | OK for LED use in this project, caution with other devices |
| GPIO12 | MTDI pin โ affects SPI voltage at boot | Best to avoid |
| GPIO15 | MTDO pin โ must be LOW at power-on | Best to avoid |
โ
List of Safe, Recommended GPIOs:
4, 5, 14, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33
(Pins 34, 35, 36, 39 are input-only โ they cannot output)