Create Your Own CCTV Camera System Using the ESP32-CAM Module

Create Your Own CCTV Camera System Using the ESP32-CAM Module

Hello and welcome back in this project we will learn how to make a DIY CCTV camera system using an ESP32-CAM module. The ESP32-CAM is a small but powerful module with built-in Wi-Fi, making it great for creating a simple remote camera system. I’ve also used a servo motor to rotate the camera module left and right, allowing us to cover a 0 to 180-degree range of view. Additionally, I’ve also set up a simple web server for this project so that you can control and view the camera from a computer or smartphone. Also, this user interface is easy to use, letting you control the camera’s position and view. Remember, this is just a basic DIY model, and you can make it better or add new features, like face detection or motion detection.

Ok, let’s do this project step by step. The required components are given below.

Step 1

Firstly, identify these components.

Step 2

Secondly, connect the ESP32-CAM board to the TTL programmer to upload the program. Refer to the circuit diagram below. After that, connect it to the computer.

Create Your Own CCTV Camera System Using the ESP32-CAM Module

Step 3

Thirdly, copy and paste the following program into the Arduino IDE. Then, enter your Wi-Fi SSID and password. Additionally, make sure to install the ESP32Servo library.

#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h"             // disable brownout problems
#include "soc/rtc_cntl_reg.h"    // disable brownout problems
#include "esp_http_server.h"
#include <ESP32Servo.h>

// Replace with your network credentials
const char* ssid = "***************";
const char* password = "**************";

#define PART_BOUNDARY "123456789000000000000987654321"
#define CAMERA_MODEL_AI_THINKER

#if defined(CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27

#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

#else
#error "Camera model not selected"
#endif

#define SERVO_1      14

#define SERVO_STEP   5

Servo servo;

int servoPos = 90;

static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t camera_httpd = NULL;
httpd_handle_t stream_httpd = NULL;

static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CCTV Security System</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 0;
      padding: 0;
      background-color: #000;
      color: #fff;
      text-align: center;
    }
    .header {
      background-color: #ff0000;
      color: #fff;
      padding: 20px 0;
    }
    .header h1 {
      margin: 0;
    }
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: flex-start; /* Adjusted to top-align content */
      min-height: 100vh; /* Allow the container to fill the entire height of the screen */
      padding-top: 40px; /* Add padding to prevent content from being too close to header */
    }
    .stream {
      margin: 10px auto;
      max-width: 100%;
      border: 2px solid #fff000;
      border-radius: 10px;
    }
    .controls {
      margin-top: 20px;
    }
    .button {
      background-color: #fff000;
      border: none;
      color: #000;
      padding: 15px 30px;
      margin: 5px;
      font-size: 16px;
      cursor: pointer;
      border-radius: 5px;
      transition: background-color 0.3s;
    }
    .button:hover {
      background-color: #cccc00;
    }
    .dropdown {
      margin-top: 15px;
      padding: 10px;
      font-size: 16px;
      background-color: #fff000;
      color: #000;
      border: 1px solid #000;
      border-radius: 5px;
    }
    /* Mobile responsiveness */
    @media screen and (max-width: 600px) {
      .container {
        flex-direction: column;
        padding: 10px;
      }
      .button {
        padding: 10px 20px;
        font-size: 14px;
      }
    }
    /* Dark Mode */
    body.dark-mode {
      background-color: #333;
      color: #fff;
    }
    .dark-mode .header {
      background-color: #ff6600;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>CCTV Security System - Powered by SriTu Hobby</h1>
  </div>
  <div class="container">
    <!-- Camera Stream -->
    <img src="" id="photo" class="stream" alt="Camera Stream">
    <!-- Camera Control -->
    <div class="controls">
      <button class="button" onclick="sendCommand('left')">Left</button>
      <button class="button" onclick="sendCommand('right')">Right</button>
      <button class="button" onclick="sendCommand('flash_on')">Flash On</button>
      <button class="button" onclick="sendCommand('flash_off')">Flash Off</button>
    </div>
    <!-- Screen Size Control -->
    <select id="screenSize" class="dropdown" onchange="changeScreenSize()">
      <option value="100">Default</option>
      <option value="75">75%</option>
      <option value="50">50%</option>
      <option value="25">25%</option>
    </select>
    <!-- Status Indicator -->
    <div id="status">Camera Status: <span id="cameraStatus">Loading...</span></div>
    <!-- Dark Mode Toggle -->
    <button class="button" onclick="toggleMode()">Toggle Dark Mode</button>
    <!-- Real-Time Log -->
    <div id="log" style="margin-top: 20px; text-align: left; max-height: 200px; overflow-y: auto;"></div>
  </div>

  <script>
    // Update the camera stream source
    window.onload = () => {
      const photo = document.getElementById("photo");
      const cameraStatus = document.getElementById("cameraStatus");
      const log = document.getElementById("log");

      // Simulate camera status check
      setTimeout(() => {
        photo.src = `${window.location.origin}:81/stream`;
        cameraStatus.innerText = "Online";
        log.innerHTML += "<p>[INFO] Camera Stream started.</p>";
      }, 2000);  // Simulate delay for stream loading

      // Set status to offline if stream fails
      photo.onerror = () => {
        cameraStatus.innerText = "Offline";
        log.innerHTML += "<p>[ERROR] Camera Stream failed.</p>";
      };
    };

    // Send camera movement command
    function sendCommand(direction) {
      fetch(`/action?go=${direction}`);
      const log = document.getElementById("log");
      log.innerHTML += `<p>[COMMAND] Camera moved ${direction}</p>`;
    }

    // Change screen size of the camera stream
    function changeScreenSize() {
      const photo = document.getElementById("photo");
      const screenSize = document.getElementById("screenSize").value;
      photo.style.width = screenSize + "%";
      const log = document.getElementById("log");
      log.innerHTML += `<p>[INFO] Screen size changed to ${screenSize}%</p>`;
    }

    //flashlight
    function sendCommand(command) {
      fetch(`/action?go=${command}`);
    }

    // Toggle dark mode
    function toggleMode() {
      document.body.classList.toggle('dark-mode');
      const log = document.getElementById("log");
      log.innerHTML += "<p>[INFO] Dark mode toggled.</p>";
    }
  </script>
</body>
</html>
)rawliteral";

static esp_err_t index_handler(httpd_req_t *req){
  httpd_resp_set_type(req, "text/html");
  return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}

static esp_err_t stream_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if(res != ESP_OK){
    return res;
  }

  while(true){
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if(fb->width > 400){
        if(fb->format != PIXFORMAT_JPEG){
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if(!jpeg_converted){
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if(res == ESP_OK){
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if(fb){
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if(_jpg_buf){
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if(res != ESP_OK){
      break;
    }
  }
  return res;
}

static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char variable[32] = {0,};

  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      if (httpd_query_key_value(buf, "go", variable, sizeof(variable)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  int res = 0;

  if(!strcmp(variable, "left")) {
    if(servoPos <= 170) {
      servoPos += 10;
      servo.write(servoPos);
      delay(10);
    }
    Serial.println(servoPos);
    Serial.println("Left");
  }
  else if(!strcmp(variable, "right")) {
    if(servoPos >= 10) {
      servoPos -= 10;
      servo.write(servoPos);
      delay(10);
    }
   
    Serial.println(servoPos);
    Serial.println("Right");
  }
  else if (!strcmp(variable, "flash_on")) {
    digitalWrite(4, HIGH);  // Turn on the flashlight
    Serial.println("Flashlight ON");
  } else if (!strcmp(variable, "flash_off")) {
    digitalWrite(4, LOW);   // Turn off the flashlight
    Serial.println("Flashlight OFF");
  }
  else {
    res = -1;
  }

  if(res){
    return httpd_resp_send_500(req);
  }

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}

void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_handler,
    .user_ctx  = NULL
  };

  httpd_uri_t cmd_uri = {
    .uri       = "/action",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };
  httpd_uri_t stream_uri = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };
  if (httpd_start(&camera_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(camera_httpd, &index_uri);
    httpd_register_uri_handler(camera_httpd, &cmd_uri);
  }
  config.server_port += 1;
  config.ctrl_port += 1;
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
  //servo.setPeriodHertz(50);    // standard 50 hz servo
  servo.attach(SERVO_1, 1000, 2000);
  pinMode(4, OUTPUT);
  digitalWrite(4, LOW);

  servo.write(servoPos);

  Serial.begin(115200);
  Serial.setDebugOutput(false);

  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;

    if(psramFound()){
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
  
  // Camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  //Rotate
  sensor_t *s = esp_camera_sensor_get();
  if(s != NULL){
    s->set_hmirror(s, 0);
    s->set_vflip(s, 1);
  }


  
  // Wi-Fi connection
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  
  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.println(WiFi.localIP());
  
  // Start streaming web server
  startCameraServer();
}

void loop() {
  
}
  • Now, select the board(AI Thinker ESP32-CAM) and port. In this step, ensure the ESP32 boards are installed in the Arduino IDE. Finally, click the upload button.
  • When the dot lines appear on the bottom bar, press and hold the reset button for one second, then release it.

Step 4

Next, remove the reset jumper wire and open the serial monitor. Then, press the reset button once. You will now see the camera URL on the serial monitor. Copy and save it in a safe place for future use.

Step 5

Next, remove all the jumper wires and the programmer. Then, prepare the DIY holder for mounting the ESP32-CAM board. After that, mount the board onto it.

Step 6

Now, prepare the base piece and install the servo motor on it. I used a piece of double-sided tape. Next, calibrate the servo motor using a PWM servo tester. Set the motor to 90 degrees as the center, then install the servo horn.

Step 7

Next, install the CAM module holder onto the servo horn.

Step 8

Afterward, connect the servo motor to the CAM board. Also, provide an external 5V power supply to the system. Refer to the circuit diagram below for guidance.

Create Your Own CCTV Camera System Using the ESP32-CAM Module

Step 9

Next, copy and paste the CAM board URL into your favorite browser. You will now see a simple camera control web server. From here, you can view the camera, rotate it, turn the camera light on and off, and adjust the screen size.

You can also view it on your smartphone. To do this, enter the URL in your mobile browser. Then, you will see it on the screen. Enjoy this project! The full video guide is below. We hope to see you in the next project. Have a great day!

Troubleshooting Tips

  • Check all connections.
  • Check the WIFI SSID and password.
  • Check the Servo Calibration.
  • Check the URL.
  • Check the Wi-Fi Connection.

Create Your Own CCTV Camera System Using the ESP32-CAM Module

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *