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.
Subscribe to my newsletter
Read articles from KINSHUK JAIN directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

KINSHUK JAIN
KINSHUK JAIN
β¨Likes to Build and scale amazing stuff .. π checkout my site : https://cloudkinshuk.in π checkout my blog : https://blog.cloudkinshuk.in Link ποΈ