How me and team Built a Smart Parking System using ESP32-CAM – A Student's Journey
Hey Everyone !
I'm Kinshuk, and in this blog, I want to take you through me and my team journey of building a Smart Parking System using an ESP32-CAM. This project combines IoT, image processing, real-time updates, and a bit of creativity.
It started as a simple idea – automate parking with a camera and make it cool enough to recognize number plates, update a web page, and even control a barrier gate. Sounds fun? Let me walk you through it, step-by-step – the way I’d explain it to any of you over a chai break. ☕
Team of the Project :
kinshuk Jain
Kumar Arnim
Pranav singh
Himanshu sharma
🧠 The Idea
Imagine you enter a parking lot – the system detects your vehicle, recognizes your number plate, opens the barrier if you're authorized, and logs your entry time. No human needed. That's what I wanted to build!
original repository: repo ( credit : circuit digest )
Forked repository : repo ( Build the project with the code base )
🛒 What we Used
ESP32-CAM: The hero of the project. Tiny, cheap, and powerful.
Servo Motor: To open and close the parking barrier.
IR Sensor / Ultrasonic Sensor: For vehicle detection.
Arduino IDE: For programming.
Web Server (hosted or local): To store images and logs.
NTP (Network Time Protocol): For real-time clock syncing.
Breadboard, jumper wires, and basic electronics gear.
Code Base :
const char* ssid = "xxx";
const char* password = "xxx";
String serverName = "www.circuitdigest.cloud";
String serverPath = "/readnumberplate";
const int serverPort = 443; // HTTPS port
String apiKey = "xxx"; // Replace xxx with your API key
String imageViewLink = "https://www.circuitdigest.cloud/static/" + apiKey + ".jpeg";
int count = 0;
WiFiClientSecure client;
// Network Time Protocol (NTP) setup
const char* ntpServer = "pool.ntp.org"; // NTP server
const long utcOffsetInSeconds = 19800; // IST offset (UTC + 5:30)
int servoPin = 14; // GPIO pin for the servo motor
int inSensor = 13; // GPIO pin for the entry sensor
int outSensor = 15; // GPIO pin for the exit sensor
Servo myservo; // Servo object
int pos = 0; // Variable to hold servo position
// Initialize the NTPClient
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, utcOffsetInSeconds);
String currentTime = "";
// Web server on port 80
WebServer server(80);
// Variables to hold recognized data, current status, and history
String recognizedPlate = ""; // Variable to store the recognized plate number
String imageLink = ""; // Variable to store the image link
String currentStatus = "Idle"; // Variable to store the current status of the system
int availableSpaces = 4; // Total parking spaces available
int vehicalCount = 0; // Number of vehicles currently parked
int barrierDelay = 3000; // Delay for barrier operations
int siteRefreshTime = 1; // Web page refresh time in seconds
// History of valid number plates and their entry times
struct PlateEntry {
String plateNumber; // Plate number of the vehicle
String time; // Entry time of the vehicle
};
std::vector<PlateEntry> plateHistory; // Vector to store the history of valid plates
// Function to extract a JSON string value by key
String extractJsonStringValue(const String& jsonString, const String& key) {
int keyIndex = jsonString.indexOf(key);
if (keyIndex == -1) {
return "";
}
int startIndex = jsonString.indexOf(':', keyIndex) + 2;
int endIndex = jsonString.indexOf('"', startIndex);
if (startIndex == -1 || endIndex == -1) {
return "";
}
return jsonString.substring(startIndex, endIndex);
}
// Function to handle the root web page
void handleRoot() {
String html = "<!DOCTYPE html><html lang='en'><head>";
html += "<meta charset='UTF-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
html += "<title>Smart Parking System</title>";
html += "<style>";
html += "body { font-family: Arial, sans-serif; background-color: #f4f4f9; margin: 0; padding: 0; color: #333; }";
html += ".container { max-width: 1200px; margin: 0 auto; padding: 20px; box-sizing: border-box; }";
html += "header { text-align: center; padding: 15px; background-color: #0e3d79; color: white; }";
html += "h1, h2 { text-align: center; margin-bottom: 20px; }"; // Center align all headers
html += "p { margin: 10px 0; }";
html += "table { width: 100%; border-collapse: collapse; margin: 20px 0; }";
html += "th, td { padding: 10px; text-align: left; border: 1px solid #ddd; }";
html += "tr:nth-child(even) { background-color: #f9f9f9; }";
html += "form { text-align: center; margin: 20px 0; }";
html += "input[type='submit'] { background-color: #007bff; color: white; border: none; padding: 10px 20px; font-size: 16px; cursor: pointer; border-radius: 5px; }";
html += "input[type='submit']:hover { background-color: #0056b3; }";
html += "a { color: #007bff; text-decoration: none; }";
html += "a:hover { text-decoration: underline; }";
html += "img { max-width: 100%; height: auto; margin: 20px 0; display: none; }"; // Initially hide the image
html += "@media (max-width: 768px) { table { font-size: 14px; } }";
html += "</style>";
html += "<meta http-equiv='refresh' content='" + String(siteRefreshTime) + "'>"; // Refresh every x second
html += "</head><body>";
html += "<header><h1>Circuit Digest</h1></header>";
html += "<div class='container'>";
html += "<h1>Smart Parking System using ESP32-CAM</h1>";
html += "<p><strong>Time:</strong> " + currentTime + "</p>";
html += "<p><strong>Status:</strong> " + currentStatus + "</p>";
html += "<p><strong>Last Recognized Plate:</strong> " + recognizedPlate + "</p>";
html += "<p><strong>Last Captured Image:</strong> <a href=\"" + imageViewLink + "\" target=\"_blank\">View Image</a></p>";
// html += "<form action=\"/trigger\" method=\"POST\">";
// html += "<input type=\"submit\" value=\"Capture Image\">";
// html += "</form>";
html += "<p><strong>Spaces available:</strong> " + String(availableSpaces - vehicalCount) + "</p>";
html += "<h2>Parking Database</h2>";
if (plateHistory.empty()) {
html += "<p>No valid number plates recognized yet.</p>";
} else {
html += "<table><tr><th>Plate Number</th><th>Time</th></tr>";
for (const auto& entry : plateHistory) {
html += "<tr><td>" + entry.plateNumber + "</td><td>" + entry.time + "</td></tr>";
}
html += "</table>";
}
html += "<script>";
html += "function toggleImage() {";
html += " var img = document.getElementById('capturedImage');";
html += " if (img.style.display === 'none') {";
html += " img.style.display = 'block';";
html += " } else {";
html += " img.style.display = 'none';";
html += " }";
html += "}";
html += "</script>";
html += "</div></body></html>";
server.send(200, "text/html", html);
}
// Function to handle image capture trigger
void handleTrigger() {
currentStatus = "Capturing Image";
server.handleClient();
// server.sendHeader("Location", "/"); // Redirect to root to refresh status
// server.send(303); // Send redirect response to refresh the page
// Perform the image capture and upload
int status = sendPhoto();
// Update status based on sendPhoto result
if (status == -1) {
currentStatus = "Image Capture Failed";
} else if (status == -2) {
currentStatus = "Server Connection Failed";
} else if (status == 1) {
currentStatus = "No Parking Space Available";
} else if (status == 2) {
currentStatus = "Invalid Plate Recognized [No Entry]";
} else {
currentStatus = "Idle";
}
server.handleClient(); // Update status on webpage
}
void openBarrier() {
currentStatus = "Barrier Opening";
server.handleClient(); // Update status on webpage
Serial.println("Barrier Opens");
myservo.write(0);
delay(barrierDelay);
}
void closeBarrier() {
currentStatus = "Barrier Closing";
server.handleClient(); // Update status on webpage
Serial.println("Barrier Closes");
myservo.write(180);
delay(barrierDelay);
}
// Function to capture and send photo to the server
int sendPhoto() {
camera_fb_t* fb = NULL;
// Turn on flashlight and capture image
// digitalWrite(flashLight, HIGH);
delay(300);
fb = esp_camera_fb_get();
delay(300);
// digitalWrite(flashLight, LOW);
if (!fb) {
Serial.println("Camera capture failed");
currentStatus = "Image Capture Failed";
server.handleClient(); // Update status on webpage
return -1;
}
// Connect to server
Serial.println("Connecting to server:" + serverName);
client.setInsecure(); // Skip certificate validation for simplicity
if (client.connect(serverName.c_str(), serverPort)) {
Serial.println("Connection successful!");
// Increment count and prepare file name
count++;
Serial.println(count);
String filename = apiKey + ".jpeg";
// Prepare HTTP POST request
String head = "--CircuitDigest\r\nContent-Disposition: form-data; name=\"imageFile\"; filename=\"" + filename + "\"\r\nContent-Type: image/jpeg\r\n\r\n";
String tail = "\r\n--CircuitDigest--\r\n";
uint32_t imageLen = fb->len;
uint32_t extraLen = head.length() + tail.length();
uint32_t totalLen = imageLen + extraLen;
client.println("POST " + serverPath + " HTTP/1.1");
client.println("Host: " + serverName);
client.println("Content-Length: " + String(totalLen));
client.println("Content-Type: multipart/form-data; boundary=CircuitDigest");
client.println("Authorization:" + apiKey);
client.println();
client.print(head);
// Send the image
currentStatus = "Uploading Image";
server.handleClient(); // Update status on webpage
// Send image data in chunks
uint8_t* fbBuf = fb->buf;
size_t fbLen = fb->len;
for (size_t n = 0; n < fbLen; n += 1024) {
if (n + 1024 < fbLen) {
client.write(fbBuf, 1024);
fbBuf += 1024;
} else {
size_t remainder = fbLen % 1024;
client.write(fbBuf, remainder);
}
}
client.print(tail);
// Release the frame buffer
esp_camera_fb_return(fb);
Serial.println("Image sent successfully");
// Waiting for server response
currentStatus = "Waiting for Server Response";
server.handleClient(); // Update status on webpage
String response = "";
long startTime = millis();
while (client.connected() && millis() - startTime < 10000) {
if (client.available()) {
char c = client.read();
response += c;
}
}
// Extract data from response
recognizedPlate = extractJsonStringValue(response, "\"number_plate\"");
imageLink = extractJsonStringValue(response, "\"view_image\"");
currentStatus = "Response Recieved Successfully";
server.handleClient(); // Update status on webpage
// Add valid plate to history
if (vehicalCount > availableSpaces) {
// Log response and return
Serial.print("Response: ");
Serial.println(response);
client.stop();
esp_camera_fb_return(fb);
return 1;
} else if (recognizedPlate.length() > 4 && recognizedPlate.length() < 11) {
// Valid plate
PlateEntry newEntry;
newEntry.plateNumber = recognizedPlate + "-Entry";
newEntry.time = currentTime; // Use the current timestamp
plateHistory.push_back(newEntry);
vehicalCount++;
openBarrier();
delay(barrierDelay);
closeBarrier();
// Log response and return
Serial.print("Response: ");
Serial.println(response);
client.stop();
esp_camera_fb_return(fb);
return 0;
} else {
currentStatus = "Invalid Plate Recognized '" + recognizedPlate + "' " + "[No Entry]";
server.handleClient(); // Update status on webpage
// Log response and return
Serial.print("Response: ");
Serial.println(response);
client.stop();
esp_camera_fb_return(fb);
return 2;
}
} else {
Serial.println("Connection to server failed");
esp_camera_fb_return(fb);
return -2;
}
}
void setup() {
// Disable brownout detector
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
pinMode(flashLight, OUTPUT);
pinMode(inSensor, INPUT_PULLUP);
pinMode(outSensor, INPUT_PULLUP);
digitalWrite(flashLight, LOW);
// Connect to WiFi
WiFi.mode(WIFI_STA);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println();
Serial.print("ESP32-CAM IP Address: ");
Serial.println(WiFi.localIP());
// Initialize NTPClient
timeClient.begin();
timeClient.update();
// Start the web server
server.on("/", handleRoot);
server.on("/trigger", HTTP_POST, handleTrigger);
server.begin();
Serial.println("Web server started");
// Configure camera
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
// Adjust frame size and quality based on PSRAM availability
if (psramFound()) {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 5; // Lower number means higher quality (0-63)
config.fb_count = 2;
Serial.println("PSRAM found");
} else {
config.frame_size = FRAMESIZE_CIF;
config.jpeg_quality = 12; // Lower number means higher quality (0-63)
config.fb_count = 1;
}
// Initialize camera
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
delay(1000);
ESP.restart();
}
// Allow allocation of all timers
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
myservo.setPeriodHertz(50); // standard 50 hz servo
myservo.attach(servoPin, 1000, 2000); // attaches the servo on pin 18 to the servo object
// Set the initial position of the servo (barrier closed)
myservo.write(180);
}
void loop() {
// Update the NTP client to get the current time
timeClient.update();
currentTime = timeClient.getFormattedTime();
// Check the web server for any incoming client requests
server.handleClient();
// Monitor sensor states for vehicle entry/exit
if (digitalRead(inSensor) == LOW && vehicalCount < availableSpaces) {
delay(2000); // delay for vehicle need to be in a position
handleTrigger(); // Trigger image capture for entry
}
if (digitalRead(outSensor) == LOW && vehicalCount > 0) {
delay(2000); // delay for vehicle need to be in a position
openBarrier();
PlateEntry newExit;
newExit.plateNumber = "NULL-Exit";
newExit.time = currentTime; // Use the current timestamp
plateHistory.push_back(newExit);
delay(barrierDelay);
vehicalCount--;
closeBarrier();
currentStatus = "Idle";
server.handleClient(); // Update status on webpage
}
}
🛠️ Step-by-Step Guide
🔌 Step 1: Setting Up the ESP32-CAM
First, my team connected the ESP32-CAM to my PC using a FTDI programmer.
In the Arduino IDE, we installed the ESP32 board package.
Selected the right board:
AI Thinker ESP32-CAM.Wrote a simple camera test sketch to check if the camera works. Spoiler: it did! 😎
#include "esp_camera.h"
// Camera config here...
📶 Step 2: WiFi Configuration
const char* ssid = "YourWiFiName";
const char* password = "YourWiFiPassword";
we set up WiFi so the ESP32-CAM can communicate with my server.
Made sure to test on a 2.4 GHz network (ESP32 doesn’t like 5 GHz).
📷 Step 3: Camera Initialization
Used the default config for AI Thinker ESP32-CAM.
Adjusted resolution and quality for number plate clarity.
Pro tip: Lower resolution gives faster uploads, but too low = unreadable plates.
🌐 Step 4: Web Server Setup
we created a simple web interface hosted on the ESP32 itself.
It shows the current parking status, a live snapshot, and logs.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html);
});
- Used the ESPAsyncWebServer library for better performance.
🕒 Step 5: Real-Time Clock with NTP
Synced the time with Indian Standard Time (IST) using an NTP server.
This helped log accurate timestamps for vehicle entries and exits.
configTime(19800, 0, "pool.ntp.org"); // IST = UTC + 5:30
📤 Step 6: Image Capture and Upload
Captured an image whenever a vehicle was detected.
Uploaded the image to my remote server using an HTTP POST request.
client.POST(imageData);
- The server then processed the image (number plate recognition can be done here with OpenCV or a simple ML model – but for this version, I just stored the images).
🚗 Step 7: Vehicle Detection + Barrier Control
Used an IR sensor to detect vehicles at the gate.
When a vehicle is detected:
Take a photo.
Check number plate (manually or using API).
If valid, servo motor rotates to open the gate.
Log the event.
servo.write(90); // Open
delay(3000);
servo.write(0); // Close
🔁 Step 8: Real-Time Web Updates
Updated the web page with:
Available parking slots
Vehicle entry/exit logs
Live image feed
Used AJAX-style refreshing to update content without reloading the page.
📋 Final Touches
Added a clean and responsive web UI.
Secured the HTTP requests with an
apiKey(basic level, for now).Deployed it in my room (LOL) for testing with a toy car. It worked surprisingly well!
⚠️ Things to Improve (Future Scope)
Integrate OCR (Optical Character Recognition) using Python/OpenCV for automatic number plate recognition.
Add Firebase or MQTT for real-time remote updates.
Use a proper SSL certificate for HTTPS.
Make a mobile app or dashboard for parking managers.
📸 Demo Time!
Here’s a sneak peek of how it looks when it’s working (GIF or photo link here if available).
🤝 Why You Should Try It Too
Whether you're from electrical, CS, or even mechanical – this project touches all the cool stuff: IoT, automation, web dev, and embedded systems. And it's super fun when it all comes together.






