How me and team Built a Smart Parking System using ESP32-CAM – A Student's Journey

KINSHUK JAINKINSHUK JAIN
12 min read

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 :

  1. kinshuk Jain

  2. Kumar Arnim

  3. Pranav singh

  4. 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:

    1. Take a photo.

    2. Check number plate (manually or using API).

    3. If valid, servo motor rotates to open the gate.

    4. 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.


0
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 πŸ–‡οΈ