Создание таймлапс-видео с ESP32-CAM

Создание таймлапс-видео с ESP32-CAM

Таймлапсы отлично подходят для фиксации медленно протекающих событий, таких как движение облаков, рост растений или ход строительства. Конечно, вы можете использовать свой смартфон и специальное приложение для создания таймлапсов, но кто захочет оставлять дорогое устройство без присмотра на несколько часов?

А что, если вы могли бы достичь тех же результатов с помощью гораздо более доступного устройства? Встречайте ESP32-CAM — мощную маленькую плату микроконтроллера с камерой и возможностями Wi-Fi — стоимостью всего около $10.

Это руководство проведёт вас через шаги по настройке ESP32-CAM для съёмки изображений через регулярные интервалы, сохранения их на SD-карту и компиляции в таймлапс-видео.

Это проще, чем вы думаете! Итак, давайте начнём!

Что вам понадобится

Для этого проекта вам понадобятся следующие компоненты:

  • Модуль ESP32-CAM: Он будет «мозгом» вашего проекта.

  • Кабель micro USB: Для питания и программирования ESP32-CAM.

  • FTDI-адаптер или адаптер ESP32-CAM-MB (опционально): Если ваша ESP32-CAM не имеет встроенного USB-to-Serial конвертера, вам понадобится это для загрузки кода.

  • Компьютер с Arduino IDE: Вы будете использовать его для написания и загрузки кода на ESP32.

  • Micro SD-карта: Для хранения снятых изображений.

При выборе платы ESP32-CAM для этого проекта важно подобрать правильный тип. Вам нужна плата, которую можно легко программировать с компьютера и запитать после этого.

ESP32-CAM с дочерней платой ESP32-CAM-MB или более новая ESP32-CAM-CH340 — хороший выбор, поскольку обе имеют USB-порт для программирования и питания. Лучше избегать «голой» платы ESP32-CAM, так как для неё нужен USB-to-Serial конвертер, которого у вас может не быть под рукой.

Подключение ESP32-CAM к компьютеру

Голый модуль ESP32-CAM, несмотря на свою мощность, не имеет встроенного USB-интерфейса для программирования и связи с компьютером. Это означает, что вам понадобится небольшая помощь для его запуска. Есть три основных варианта на выбор:

Вариант 1: ESP32-CAM + FTDI-адаптер

Если у вас есть голый модуль ESP32-CAM, вы можете использовать USB-to-serial адаптер (FTDI-адаптер) для подключения к компьютеру следующим образом:

Подключение ESP32-CAM к FTDI-адаптеру

Многие FTDI-адаптеры имеют перемычку, позволяющую выбирать между 3,3 В и 5 В. Поскольку мы запитываем ESP32-CAM от 5 В, убедитесь, что перемычка установлена на 5 В.

Обратите внимание, что вывод GPIO 0 подключён к земле. Это подключение необходимо только при программировании ESP32-CAM. После завершения программирования модуля вы должны отключить это подключение.

Запомните! Вам придётся выполнять это подключение каждый раз, когда вы хотите загрузить новый скетч.

Вариант 2: ESP32-CAM + адаптер ESP32-CAM-MB

Использование FTDI-адаптера для программирования ESP32-CAM — это немного хлопотно. Вот почему многие продавцы теперь продают плату ESP32-CAM вместе с маленькой дочерней платой под названием ESP32-CAM-MB.

Вы устанавливаете ESP32-CAM на дочернюю плату, подключаете кабель micro USB и нажимаете кнопку Upload для программирования платы. Всё так просто.

Обзор оборудования программатора ESP32-CAM-MB

Вариант 3: ESP32-CAM-CH340

Если у вас модуль ESP32-CAM-CH340, вам повезло! Этот вариант поставляется с чипом CH340 USB-to-serial прямо на плате, что исключает необходимость дополнительного оборудования. Просто подключите его к компьютеру через кабель micro USB — и вы готовы к работе.

ESP32-CAM-CH340

Имейте в виду, что вам всё равно нужно подключить вывод GPIO0 к GND при программировании, как и с FTDI-адаптером.

Настройка Arduino IDE

Независимо от выбранного варианта, настройка в Arduino IDE одинакова.

Установка платы ESP32

Для использования ESP32-CAM или любой ESP32 с Arduino IDE вы должны сначала установить плату ESP32 (также известную как ESP32 Arduino Core) через Arduino Board Manager.

Если вы ещё этого не сделали, следуйте этому руководству для установки платы ESP32:

Установка платы ESP32 в Arduino IDE

Выбор платы и порта

После установки ESP32 Arduino Core перезапустите Arduino IDE и нажмите «Select other board and port…» в верхнем выпадающем меню.

Меню выбора платы и порта в Arduino IDE 2

Появится новое окно. Найдите конкретный тип платы ESP32, который вы используете (в нашем случае это AI Thinker ESP32-CAM).

Далее выберите порт, соответствующий вашей плате ESP32-CAM. Обычно он обозначен как «/dev/ttyUSB0» (в Linux или macOS) или «COM6» (в Windows).

Выбор платы и порта ESP32-CAM в Arduino IDE 2

Вот и всё; Arduino IDE теперь настроена для ESP32-CAM!

Загрузка кода

Теперь перейдём к созданию таймлапса! Ниже приведён код, который это сделает.

По умолчанию этот код настроен на съёмку одного кадра каждые 30 минут. Если вы хотите изменить это, отредактируйте число в строке #define MINUTES_BETWEEN_PHOTOS 30.

#include "esp_camera.h"
#include "FS.h"                // SD Card ESP32
#include "SD_MMC.h"            // SD Card ESP32
#include "soc/soc.h"           // Disable brownout problems
#include "soc/rtc_cntl_reg.h"  // Disable brownout problems
#include "driver/rtc_io.h"

#define MINUTES_BETWEEN_PHOTOS 30

// Pin definition for 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

// Flash LED pin
#define FLASH_LED_PIN 4

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);  // Disable brownout detector

  // Initialize serial
  Serial.begin(115200);
  while (!Serial) delay(100);

  // Initialize SD Card BEFORE camera to reduce memory pressure
  startMicroSD();

  // Initialize camera
  startCamera();

  delay(2000);  // Delay 2 seconds before first photo
}

void loop() {
  // Keep a count of the number of photos we have taken
  static int number = 0;
  number++;

  // Construct a filename that looks like "/photo_0001.jpg"

  String filename = "/photo_";
  if (number < 1000) filename += "0";
  if (number < 100) filename += "0";
  if (number < 10) filename += "0";
  filename += number;
  filename += ".jpg";

  takePhoto(filename);

  // Delay until the next photo
  delay(MINUTES_BETWEEN_PHOTOS * 60 * 1000);
}

bool startMicroSD() {
  Serial.print("Starting microSD... ");

  // Pin 13 needs to be pulled-up
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/sd_pullup_requirements.html#pull-up-conflicts-on-gpio13
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);

  if (SD_MMC.begin("/sdcard", true)) {
    Serial.println("OKAY");
    return true;
  } else {
    Serial.println("FAILED");
    return false;
  }
}

void startCamera() {
  // Turn off the flash
  pinMode(FLASH_LED_PIN, OUTPUT);
  digitalWrite(FLASH_LED_PIN, LOW);

  // Camera configuration - REDUCED settings to prevent stack overflow
  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_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_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;

  // CRITICAL: Reduced frame size and quality to prevent stack overflow
  if (psramFound()) {
    Serial.println("PSRAM found");
    config.frame_size = FRAMESIZE_XGA;  // Reduced from UXGA to XGA
    config.jpeg_quality = 12;           // Increased from 10 to 12 (lower quality, smaller file)
    config.fb_count = 1;                // Reduced from 2 to 1 to save memory
  } else {
    Serial.println("PSRAM not found");
    config.frame_size = FRAMESIZE_VGA;  // Reduced from SVGA to VGA
    config.jpeg_quality = 15;           // Lower quality for boards without PSRAM
    config.fb_count = 1;
  }

  // Additional memory-saving configurations
  config.grab_mode = CAMERA_GRAB_LATEST;  // Changed from WHEN_EMPTY to LATEST

  // Initialize Camera
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x\n", err);
    return;
  }
  Serial.println("Camera initialized successfully");

  // Additional sensor settings to optimize for memory usage
  sensor_t *s = esp_camera_sensor_get();
  if (s != NULL) {
    // Lower the resolution if needed
    // s->set_framesize(s, FRAMESIZE_VGA);

    // Optimize other settings
    s->set_brightness(s, 0);                  // -2 to 2
    s->set_contrast(s, 0);                    // -2 to 2
    s->set_saturation(s, 0);                  // -2 to 2
    s->set_special_effect(s, 0);              // 0 to 6 (0 - No Effect)
    s->set_whitebal(s, 1);                    // 0 = disable , 1 = enable
    s->set_awb_gain(s, 1);                    // 0 = disable , 1 = enable
    s->set_wb_mode(s, 0);                     // 0 to 4 - if awb_gain enabled
    s->set_exposure_ctrl(s, 1);               // 0 = disable , 1 = enable
    s->set_aec2(s, 0);                        // 0 = disable , 1 = enable
    s->set_ae_level(s, 0);                    // -2 to 2
    s->set_aec_value(s, 300);                 // 0 to 1200
    s->set_gain_ctrl(s, 1);                   // 0 = disable , 1 = enable
    s->set_agc_gain(s, 0);                    // 0 to 30
    s->set_gainceiling(s, (gainceiling_t)0);  // 0 to 6
    s->set_bpc(s, 0);                         // 0 = disable , 1 = enable
    s->set_wpc(s, 1);                         // 0 = disable , 1 = enable
    s->set_raw_gma(s, 1);                     // 0 = disable , 1 = enable
    s->set_lenc(s, 1);                        // 0 = disable , 1 = enable
    s->set_hmirror(s, 0);                     // 0 = disable , 1 = enable
    s->set_vflip(s, 0);                       // 0 = disable , 1 = enable
    s->set_dcw(s, 1);                         // 0 = disable , 1 = enable
    s->set_colorbar(s, 0);                    // 0 = disable , 1 = enable
  }
}

void takePhoto(String filename) {

  digitalWrite(FLASH_LED_PIN, HIGH);

  // Take Picture with Camera
  Serial.println("Taking picture...");
  camera_fb_t *fb = NULL;

  // Try to take picture with retries
  for (int retry = 0; retry < 3; retry++) {
    fb = esp_camera_fb_get();
    if (fb) break;
    Serial.printf("Camera capture failed, retry %d\n", retry + 1);
    delay(500);
  }

  digitalWrite(FLASH_LED_PIN, LOW);

  if (!fb) {
    Serial.println("Camera capture failed after retries");
    return;
  }
  Serial.printf("Picture taken! Size: %zu bytes\n", fb->len);

  // Save picture to SD card
  fs::FS &fs = SD_MMC;
  Serial.printf("Picture file name: %s\n", filename.c_str());

  File file = fs.open(filename.c_str(), FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file in writing mode");
    esp_camera_fb_return(fb);
    return;
  }

  // Write file in chunks to prevent memory issues
  size_t totalBytes = fb->len;
  size_t bytesWritten = 0;
  const size_t chunkSize = 1024;  // Write in 1KB chunks

  while (bytesWritten < totalBytes) {
    size_t toWrite = min(chunkSize, totalBytes - bytesWritten);
    size_t written = file.write(fb->buf + bytesWritten, toWrite);
    if (written != toWrite) {
      Serial.println("Write error occurred");
      break;
    }
    bytesWritten += written;
  }

  file.close();

  if (bytesWritten == totalBytes) {
    Serial.printf("Saved file to path: %s (%zu bytes)\n", filename.c_str(), bytesWritten);

    Serial.println("Picture saved successfully!");
  } else {
    Serial.printf("File write incomplete: %zu/%zu bytes\n", bytesWritten, totalBytes);
  }

  // Return the frame buffer immediately after use
  esp_camera_fb_return(fb);
}

Объяснение кода

Скетч начинается с подключения нескольких библиотек.

  • Библиотека esp_camera.h позволяет управлять камерой ESP32-CAM. Она включает все инструменты для съёмки фотографий, настройки параметров камеры и работы с данными изображений.

  • Arduino.h — стандартная библиотека Arduino, включающая базовые функции, такие как delay(), Serial.print() и другие, которые мы используем почти в каждой программе Arduino.

  • Библиотека FS.h предоставляет команды для чтения и записи файлов на SD-карту.

  • Библиотека SD_MMC.h помогает работать с SD-картами через интерфейс SD_MMC, который ESP32-CAM использует для связи со слотом microSD-карты.

  • Библиотеки soc.h и rtc_cntl_reg.h позволяют отключить детектор пониженного напряжения (brownout detector), который может вызывать нежелательные сбросы при падении напряжения платы.

  • Библиотека rtc_io.h помогает управлять выводами RTC (Real Time Clock), что полезно, когда мы хотим удерживать GPIO в режиме глубокого сна — например, чтобы светодиод вспышки оставался выключенным.

#include "esp_camera.h"
#include "Arduino.h"
#include "FS.h"                // SD Card ESP32
#include "SD_MMC.h"            // SD Card ESP32
#include "soc/soc.h"           // Disable brownout problems
#include "soc/rtc_cntl_reg.h"  // Disable brownout problems
#include "driver/rtc_io.h"

Затем определяются некоторые константы. MINUTES_BETWEEN_PHOTOS определяет временной интервал (в минутах) между каждым снимком и установлен на 30 минут.

#define MINUTES_BETWEEN_PHOTOS 30

Далее мы определяем все выводы, которые плата ESP32-CAM использует для связи с камерой и управления другими функциями. Эти номера выводов специфичны для модели AI-Thinker, поэтому мы убеждаемся, что они совпадают. Мы также определяем вывод для светодиода вспышки, который помогает делать фотографии при слабом освещении.

// Pin definition for 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

// Flash LED pin
#define FLASH_LED_PIN 4

Далее определена функция startMicroSD(). Эта функция запускает обмен данными с microSD-картой. Сначала вывод 13 устанавливается в HIGH для включения SD-карты. Затем SD-карта инициализируется. В случае успеха она выводит «OKAY» и возвращает true, в противном случае выводит «FAILED» и возвращает false.

bool startMicroSD() {
  Serial.print("Starting microSD... ");

  // Pin 13 needs to be pulled-up
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/sd_pullup_requirements.html#pull-up-conflicts-on-gpio13
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);

  if (SD_MMC.begin("/sdcard", true)) {
    Serial.println("OKAY");
    return true;
  } else {
    Serial.println("FAILED");
    return false;
  }
}

Затем определена функция startCamera(). Эта функция настраивает модуль камеры. Сначала вспышка камеры выключается. Затем создаётся структура конфигурации camera_config_t и заполняется различными настройками камеры (разрешение, назначение выводов, формат пикселей и т.д.). Разрешение устанавливается в зависимости от наличия дополнительной памяти (PSRAM). Если PSRAM найдена, используется более высокое разрешение. Если нет, мы снижаем настройки, чтобы плата не вылетала из-за чрезмерного использования памяти. Наконец, камера инициализируется. В случае успеха она выводит «OKAY» и возвращает true, в противном случае выводит «FAILED» и возвращает false.

void startCamera() {
  // Turn off the flash
  pinMode(FLASH_LED_PIN, OUTPUT);
  digitalWrite(FLASH_LED_PIN, LOW);

  // Camera configuration - REDUCED settings to prevent stack overflow
  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_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_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;

  // CRITICAL: Reduced frame size and quality to prevent stack overflow
  if (psramFound()) {
    Serial.println("PSRAM found");
    config.frame_size = FRAMESIZE_XGA;  // Reduced from UXGA to XGA
    config.jpeg_quality = 12;           // Increased from 10 to 12 (lower quality, smaller file)
    config.fb_count = 1;                // Reduced from 2 to 1 to save memory
  } else {
    Serial.println("PSRAM not found");
    config.frame_size = FRAMESIZE_VGA;  // Reduced from SVGA to VGA
    config.jpeg_quality = 15;           // Lower quality for boards without PSRAM
    config.fb_count = 1;
  }

  // Additional memory-saving configurations
  config.grab_mode = CAMERA_GRAB_LATEST;  // Changed from WHEN_EMPTY to LATEST

  // Initialize Camera
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x\n", err);
    return;
  }
  Serial.println("Camera initialized successfully");

  // Additional sensor settings to optimize for memory usage
  sensor_t *s = esp_camera_sensor_get();
  if (s != NULL) {
    // Lower the resolution if needed
    // s->set_framesize(s, FRAMESIZE_VGA);

    // Optimize other settings
    s->set_brightness(s, 0);                  // -2 to 2
    s->set_contrast(s, 0);                    // -2 to 2
    s->set_saturation(s, 0);                  // -2 to 2
    s->set_special_effect(s, 0);              // 0 to 6 (0 - No Effect)
    s->set_whitebal(s, 1);                    // 0 = disable , 1 = enable
    s->set_awb_gain(s, 1);                    // 0 = disable , 1 = enable
    s->set_wb_mode(s, 0);                     // 0 to 4 - if awb_gain enabled
    s->set_exposure_ctrl(s, 1);               // 0 = disable , 1 = enable
    s->set_aec2(s, 0);                        // 0 = disable , 1 = enable
    s->set_ae_level(s, 0);                    // -2 to 2
    s->set_aec_value(s, 300);                 // 0 to 1200
    s->set_gain_ctrl(s, 1);                   // 0 = disable , 1 = enable
    s->set_agc_gain(s, 0);                    // 0 to 30
    s->set_gainceiling(s, (gainceiling_t)0);  // 0 to 6
    s->set_bpc(s, 0);                         // 0 = disable , 1 = enable
    s->set_wpc(s, 1);                         // 0 = disable , 1 = enable
    s->set_raw_gma(s, 1);                     // 0 = disable , 1 = enable
    s->set_lenc(s, 1);                        // 0 = disable , 1 = enable
    s->set_hmirror(s, 0);                     // 0 = disable , 1 = enable
    s->set_vflip(s, 0);                       // 0 = disable , 1 = enable
    s->set_dcw(s, 1);                         // 0 = disable , 1 = enable
    s->set_colorbar(s, 0);                    // 0 = disable , 1 = enable
  }
}

Далее определена функция takePhoto(String filename). Эта функция захватывает и сохраняет фотографию. Сначала она делает снимок с помощью esp_camera_fb_get(). Проверяет, что формат захваченного изображения — JPEG. Если формат верный, открывает файл на SD-карте с заданным именем в режиме записи. Данные изображения записываются в файл. Вспышка мигает, указывая, что фотография была сделана. Файл закрывается, и буфер камеры возвращается для освобождения памяти.

void takePhoto(String filename) {

  digitalWrite(FLASH_LED_PIN, HIGH);

  // Take Picture with Camera
  Serial.println("Taking picture...");
  camera_fb_t *fb = NULL;

  // Try to take picture with retries
  for (int retry = 0; retry < 3; retry++) {
    fb = esp_camera_fb_get();
    if (fb) break;
    Serial.printf("Camera capture failed, retry %d\n", retry + 1);
    delay(500);
  }

  digitalWrite(FLASH_LED_PIN, LOW);

  if (!fb) {
    Serial.println("Camera capture failed after retries");
    return;
  }
  Serial.printf("Picture taken! Size: %zu bytes\n", fb->len);

  // Save picture to SD card
  fs::FS &fs = SD_MMC;
  Serial.printf("Picture file name: %s\n", filename.c_str());

  File file = fs.open(filename.c_str(), FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file in writing mode");
    esp_camera_fb_return(fb);
    return;
  }

  // Write file in chunks to prevent memory issues
  size_t totalBytes = fb->len;
  size_t bytesWritten = 0;
  const size_t chunkSize = 1024;  // Write in 1KB chunks

  while (bytesWritten < totalBytes) {
    size_t toWrite = min(chunkSize, totalBytes - bytesWritten);
    size_t written = file.write(fb->buf + bytesWritten, toWrite);
    if (written != toWrite) {
      Serial.println("Write error occurred");
      break;
    }
    bytesWritten += written;
  }

  file.close();

  if (bytesWritten == totalBytes) {
    Serial.printf("Saved file to path: %s (%zu bytes)\n", filename.c_str(), bytesWritten);

    Serial.println("Picture saved successfully!");
  } else {
    Serial.printf("File write incomplete: %zu/%zu bytes\n", bytesWritten, totalBytes);
  }

  // Return the frame buffer immediately after use
  esp_camera_fb_return(fb);
}

В функции setup() мы начинаем с отключения детектора пониженного напряжения — это функция, которая сбрасывает плату, если напряжение падает слишком низко. Иногда это может вызывать проблемы при нормальном использовании, поэтому мы отключаем её для более плавной работы. Далее инициализируется microSD-карта, запускается и настраивается модуль камеры. Наконец, реализована 2-секундная задержка, чтобы все компоненты были полностью инициализированы перед первым снимком.

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);  // Disable brownout detector

  // Initialize serial
  Serial.begin(115200);
  while (!Serial) delay(100);

  // Initialize SD Card BEFORE camera to reduce memory pressure
  startMicroSD();

  // Initialize camera
  startCamera();

  delay(2000);  // Delay 2 seconds before first photo
}

Функция loop() управляет непрерывным процессом съёмки. Сначала она увеличивает счётчик для отслеживания общего количества сделанных фотографий. Затем формирует уникальное имя файла для нового снимка, например «/photo_0001.jpg». После этого вызывает функцию takePhoto(filename) для захвата и сохранения изображения на SD-карту. Наконец, приостанавливает выполнение на время, заданное переменной MINUTES_BETWEEN_PHOTOS, создавая интервал между снимками.

void loop() {
  // Keep a count of the number of photos we have taken
  static int number = 0;
  number++;

  // Construct a filename that looks like "/photo_0001.jpg"

  String filename = "/photo_";
  if (number < 1000) filename += "0";
  if (number < 100) filename += "0";
  if (number < 10) filename += "0";
  filename += number;
  filename += ".jpg";

  takePhoto(filename);

  // Delay until the next photo
  delay(MINUTES_BETWEEN_PHOTOS * 60 * 1000);
}

Запись таймлапса

После загрузки кода на ESP32-CAM отключите модуль, вставьте micro SD-карту и снова подключите. Подождите две секунды, и если всё в порядке, вы увидите, как белый светодиод мигнёт один раз, сигнализируя о том, что фотография сохранена на карту. Если вы не меняли тайминг, следующая фотография будет сделана через полчаса.

Прежде чем начать запись таймлапса по-настоящему, давайте выполним быструю проверку. Извлеките SD-карту из камеры и вставьте её в компьютер. Убедитесь, что вы видите файл фотографии с именем «photo_0001.jpg» на карте. Если он есть, вы готовы к работе!

Фотография таймлапса ESP32-CAM, сохранённая на SD-карту

Теперь установите ESP32-CAM в нужном месте и запитайте его от USB-зарядки. Подождите, пока светодиод мигнёт один раз, подтверждая, что первый кадр был снят. Теперь можете оставить его записывать столько, сколько хотите!

Помните, каждый раз, когда вы извлекаете и вставляете обратно SD-карту, вам нужно отключить питание ESP32-CAM, чтобы начать новую запись. Просто отключите и снова подключите кабель или зарядку, и наблюдайте за миганием светодиода, чтобы знать, что устройство работает.

Создание видео

Когда вы накопите достаточно изображений для создания таймлапса, извлеките SD-карту из ESP32-CAM и перенесите файлы на компьютер.

Затем вы можете использовать программу для редактирования видео, чтобы собрать изображения в финальное видео. Доступно несколько вариантов — от более продвинутых инструментов, таких как FFmpeg и OpenShot, до более простых онлайн-видеоредакторов. Выберите тот, который лучше всего подходит вашим потребностям и уровню навыков, и позвольте магии таймлапса раскрыться!

Дальнейшее развитие

Хотя этот проект показывает простой способ создания таймлапса с помощью ESP32-CAM, есть несколько способов сделать его ещё лучше.

Например, вы можете модифицировать код, чтобы ESP32-CAM переходила в режим глубокого сна между снимками, просыпаясь только когда приходит время сделать фотографию. Это значительно снизит энергопотребление, сделав вашу таймлапс-установку более портативной и дружественной к батарее.

Другая идея — использовать беспроводные возможности ESP32-CAM для добавления веб-интерфейса. Этот интерфейс мог бы позволить вам скачивать фотографии удалённо или даже использовать смартфон в качестве видоискателя. Таким образом, вы можете легко кадрировать объект во время настройки, не извлекая SD-карту и не начиная всё сначала каждый раз.