ESP32-CAM: съёмка и сохранение фото на microSD

ESP32-CAM — съёмка и сохранение фотографий на microSD-карту

Создаёте ли вы фотоловушку для дикой природы, проект таймлапса или просто хотите исследовать возможности IoT-фотографии с помощью ESP32-CAM — одно ясно: вам нужен простой способ снимать и сохранять фотографии на microSD-карту. Именно этим мы и займёмся в данном руководстве.

Давайте начнём!

Обзор проекта

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

Обзор проекта сохранения фотографий на microSD-карту с ESP32-CAM

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

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

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

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

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

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

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

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

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

Подготовка microSD-карты

Прежде чем использовать microSD-карту в проекте, важно убедиться, что она правильно отформатирована в файловую систему FAT16 или FAT32. Это поможет ESP32 записывать файлы без каких-либо проблем.

Если вы используете новую SD-карту, она, скорее всего, уже отформатирована в файловую систему FAT. Однако заводское форматирование может быть неидеальным, и вы можете столкнуться с проблемами. Если вы используете старую карту, которая уже использовалась ранее, её определённо нужно переформатировать. В любом случае рекомендуется отформатировать карту перед использованием в проекте.

Существует два способа форматирования microSD-карты:

Способ 1

Сначала вставьте microSD-карту в компьютер. Затем найдите диск вашей SD-карты и щёлкните по нему правой кнопкой мыши. Выберите «Форматировать» из меню. Появится окно — выберите FAT32 в качестве файловой системы, затем нажмите «Начать» для начала форматирования. Следуйте инструкциям на экране до завершения.

Форматирование SD-карты в Windows

Способ 2

Для лучших результатов и меньшего количества ошибок настоятельно рекомендуется использовать официальную утилиту форматирования SD-карт, созданную SD Association. Этот инструмент более надёжен, чем стандартная утилита форматирования вашего компьютера. Вы можете скачать его с сайта SD Association. После установки запустите программу, выберите вашу SD-карту из списка дисков и нажмите кнопку «Format». Этот специальный инструмент помогает избежать распространённых проблем, вызванных плохим или неполным форматированием, что может сэкономить вам много времени на устранение неполадок в дальнейшем.

Скриншот SD Formatter

Подключение 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!

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

Ниже приведён код, который вам нужно загрузить на ESP32-CAM.

#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"
#include <EEPROM.h>            // read and write from flash memory

// Define the number of bytes you want to access
#define EEPROM_SIZE 1

// 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

int pictureNumber = 0;

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

  Serial.begin(115200);
  Serial.println("ESP32-CAM Photo Capture Starting...");

  // Initialize EEPROM first to get picture number
  EEPROM.begin(EEPROM_SIZE);
  pictureNumber = EEPROM.read(0) + 1;

  // Handle overflow (reset to 1 if it exceeds 255)
  if (pictureNumber > 255) {
    pictureNumber = 1;
  }

  Serial.printf("Taking picture number: %d\n", pictureNumber);

  // Initialize SD Card BEFORE camera to reduce memory pressure
  Serial.println("Starting SD Card");
  if (!SD_MMC.begin()) {
    Serial.println("SD Card Mount Failed");
    goToSleep();
    return;
  }

  uint8_t cardType = SD_MMC.cardType();
  if (cardType == CARD_NONE) {
    Serial.println("No SD Card attached");
    goToSleep();
    return;
  }

  Serial.printf("SD Card Type: ");
  if (cardType == CARD_MMC) {
    Serial.println("MMC");
  } else if (cardType == CARD_SD) {
    Serial.println("SDSC");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }

  // 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);
    goToSleep();
    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
  }

  // Wait a moment for camera to stabilize
  delay(1000);

  // 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);
  }

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

  // Create file path
  String path = "/picture" + String(pictureNumber) + ".jpg";

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

  File file = fs.open(path.c_str(), FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file in writing mode");
    esp_camera_fb_return(fb);
    goToSleep();
    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", path.c_str(), bytesWritten);

    // Update picture number in EEPROM only after successful save
    EEPROM.write(0, pictureNumber);
    EEPROM.commit();
    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);

  // Brief delay before sleep
  delay(500);

  goToSleep();
}

void goToSleep() {
  // Deinitialize camera to free memory before sleep
  esp_camera_deinit();

  // Turn off the ESP32-CAM white on-board LED (flash) connected to GPIO 4
  pinMode(FLASH_LED_PIN, OUTPUT);
  digitalWrite(FLASH_LED_PIN, LOW);
  rtc_gpio_hold_en(GPIO_NUM_4);

  Serial.println("Going to sleep now");
  Serial.flush();

  delay(100);

  esp_deep_sleep_start();
}

void loop() {
  // Empty loop - everything happens in setup()
}

Демонстрация

Когда будете готовы, нажмите кнопку «Upload» в Arduino IDE для передачи кода на ESP32-CAM. После завершения загрузки, если вы используете FTDI-адаптер или модуль ESP32-CAM-CH340, обязательно отключите вывод GPIO 0 от GND. Это важно для корректной работы ESP32-CAM в нормальном режиме.

Далее откройте Монитор порта в Arduino IDE и установите скорость передачи данных 115200. Чтобы ESP32-CAM начала выполнять только что загруженный код, быстро нажмите кнопку RST. Камера сделает фотографию. Когда она делает снимок, вы увидите, как вспышка (подключённая к GPIO 4) кратковременно включится.

Проверьте Монитор порта на наличие сообщений. Вы должны увидеть имя файла фотографии вместе с сообщением, подтверждающим, что изображение сохранено на microSD-карту.

Вывод монитора порта при сохранении фотографий на microSD-карту ESP32-CAM

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

Фотографии на microSD-карте ESP32-CAM

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

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

  • Библиотека 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 в режиме глубокого сна — например, чтобы светодиод вспышки оставался выключенным.

  • Библиотека EEPROM.h позволяет сохранять данные (например, номер снимка) во внутреннюю флеш-память ESP32, чтобы они не стирались при сбросе платы. Таким образом, каждая новая фотография получает новое имя файла, а не начинается с 0.

#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"
#include <EEPROM.h>            // read and write from flash memory

Мы также определяем, сколько байт EEPROM мы хотим использовать — в данном случае всего 1 байт достаточно для хранения чисел до 255.

// Define the number of bytes you want to access
#define EEPROM_SIZE 1

Затем мы определяем все выводы, которые плата 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

Мы также создаём переменную pictureNumber, которую будем использовать для отслеживания количества сделанных фотографий.

int pictureNumber = 0;

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

WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);  // Disable brownout detector

Далее мы запускаем Serial Monitor, чтобы видеть сообщения от платы в Arduino IDE. Мы также выводим приветственное сообщение, чтобы знать, что программа запустилась.

Serial.begin(115200);
Serial.println("ESP32-CAM Photo Capture Starting...");

Затем мы инициализируем EEPROM для чтения последнего сохранённого номера фотографии. Мы добавляем 1 к этому номеру, чтобы следующая фотография получила новое имя. Если это число когда-либо станет слишком большим (больше 255), мы сбрасываем его обратно до 1, чтобы не возникало ошибок.

// Initialize EEPROM first to get picture number
EEPROM.begin(EEPROM_SIZE);
pictureNumber = EEPROM.read(0) + 1;

// Handle overflow (reset to 1 if it exceeds 255)
if (pictureNumber > 255) {
  pictureNumber = 1;
}

Serial.printf("Taking picture number: %d\n", pictureNumber);

Далее мы настраиваем SD-карту. Этот шаг очень важен и должен быть выполнен до инициализации камеры, поскольку камера использует много памяти.

Если SD-карта не монтируется должным образом или карта не вставлена, мы выводим ошибку и отправляем плату в режим глубокого сна для экономии энергии.

// Initialize SD Card BEFORE camera to reduce memory pressure
Serial.println("Starting SD Card");
if (!SD_MMC.begin()) {
  Serial.println("SD Card Mount Failed");
  goToSleep();
  return;
}

Когда SD-карта готова, мы проверяем её тип и выводим, какой она — это помогает при отладке.

uint8_t cardType = SD_MMC.cardType();
if (cardType == CARD_NONE) {
  Serial.println("No SD Card attached");
  goToSleep();
  return;
}

Serial.printf("SD Card Type: ");
if (cardType == CARD_MMC) {
  Serial.println("MMC");
} else if (cardType == CARD_SD) {
  Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
  Serial.println("SDHC");
} else {
  Serial.println("UNKNOWN");
}

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

// 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;

Затем мы проверяем, доступна ли PSRAM (дополнительная память) на плате. Если да, мы используем лучшие настройки, такие как более высокое разрешение и лучшее качество 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);
  goToSleep();
  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
}

Мы ждём секунду, чтобы камера стабилизировалась, и затем делаем снимок.

// Wait a moment for camera to stabilize
delay(1000);

Мы пытаемся до трёх раз сделать снимок, на случай если первая попытка не удастся. Если это продолжает не работать, мы переходим в сон. Но если получится, мы выводим размер только что сделанного изображения.

// 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);
}

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

Теперь мы формируем путь к файлу, используя номер снимка, например «/picture3.jpg», и открываем новый файл на SD-карте с этим именем.

// Create file path
String path = "/picture" + String(pictureNumber) + ".jpg";

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

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

Затем мы сохраняем фотографию, записывая её небольшими фрагментами по 1 килобайту (1024 байта). Это помогает предотвратить проблемы с памятью и не допустить сбоя платы при больших записях.

// 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;
}

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

file.close();

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

  // Update picture number in EEPROM only after successful save
  EEPROM.write(0, pictureNumber);
  EEPROM.commit();
  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);

// Brief delay before sleep
delay(500);

goToSleep();

Функция goToSleep() выключает камеру и светодиод вспышки, выводит сообщение о переходе в сон и затем запускает режим глубокого сна. Это означает, что плата будет выключена до повторного нажатия кнопки RESET.

void goToSleep() {
// Deinitialize camera to free memory before sleep
esp_camera_deinit();

// Turn off the ESP32-CAM white on-board LED (flash) connected to GPIO 4
pinMode(FLASH_LED_PIN, OUTPUT);
digitalWrite(FLASH_LED_PIN, LOW);
rtc_gpio_hold_en(GPIO_NUM_4);

Serial.println("Going to sleep now");
Serial.flush();

delay(100);

esp_deep_sleep_start();
}

Наконец, у нас есть пустая функция loop(), потому что всё необходимое выполняется в части setup().

void loop() {
// Empty loop - everything happens in setup()
}