ESP32-CAM: съёмка фото и отображение на веб-сервере

Узнайте, как создать веб-сервер на плате ESP32-CAM, который позволяет отправить команду на съёмку фото и просмотреть последний сделанный снимок в браузере, сохранённый в SPIFFS. Мы также добавили возможность поворота изображения при необходимости.

ESP32-CAM съёмка фото и отображение на веб-сервере

У нас есть другие проекты с ESP32-CAM в блоге, которые могут вам понравиться. Фактически, вы можете развить этот проект, добавив PIR-датчик для съёмки фото при обнаружении движения, физическую кнопку для съёмки фото или включив возможности потокового видео по другому URL-пути.

Другие проекты и руководства по ESP32-CAM:

Смотрите видео-демонстрацию

Посмотрите следующее видео-демонстрацию, чтобы увидеть, что вы будете создавать в этом руководстве.

Необходимые компоненты

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

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

Следующее изображение показывает веб-сервер, который мы создадим в этом руководстве.

ESP32-CAM веб-сервер — отображение последнего сделанного фото

Когда вы откроете веб-сервер, вы увидите три кнопки:

  • ROTATE: в зависимости от ориентации вашего ESP32-CAM, вам может потребоваться повернуть фото;

  • CAPTURE PHOTO: при нажатии на эту кнопку ESP32-CAM делает новое фото и сохраняет его в SPIFFS ESP32. Пожалуйста, подождите не менее 5 секунд перед обновлением веб-страницы, чтобы убедиться, что ESP32-CAM сделал и сохранил фото;

  • REFRESH PAGE: при нажатии на эту кнопку веб-страница обновляется и показывает последнее фото.

Примечание

Как упоминалось ранее, последнее сделанное фото хранится в SPIFFS ESP32, поэтому даже при перезагрузке платы вы всегда можете получить доступ к последнему сохранённому фото.

Установка дополнения ESP32

Мы будем программировать плату ESP32 с помощью Arduino IDE. Поэтому вам нужна установленная Arduino IDE, а также дополнение ESP32:

Установка библиотек

Мы построим веб-сервер с использованием следующих библиотек:

Вы можете установить эти библиотеки через Arduino Library Manager. Откройте Library Manager, нажав на значок библиотеки в левой боковой панели.

Найдите ESPAsyncWebServer и установите ESPAsyncWebServer от ESP32Async.

Установка ESPAsyncWebServer ESP32 Arduino IDE

Затем установите библиотеку AsyncTCP. Найдите AsyncTCP и установите AsyncTCP от ESP32Async.

Установка AsyncTCP ESP32 Arduino IDE

Скетч веб-сервера ESP32-CAM для съёмки и отображения фото

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

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-cam-take-photo-display-web-server/

  IMPORTANT!!!
   - Select Board "AI Thinker ESP32-CAM"
   - GPIO 0 must be connected to GND to upload a sketch
   - After connecting GPIO 0 to GND, press the ESP32-CAM on-board RESET button to put your board in flashing mode

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

#include "WiFi.h"
#include "esp_camera.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "soc/soc.h"           // Disable brownour problems
#include "soc/rtc_cntl_reg.h"  // Disable brownour problems
#include "driver/rtc_io.h"
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <FS.h>

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

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

boolean takeNewPhoto = false;

// Photo File Name to save in SPIFFS
#define FILE_PHOTO "/photo.jpg"

// OV2640 camera module pins (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

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { text-align:center; }
    .vert { margin-bottom: 10%; }
    .hori{ margin-bottom: 0%; }
  </style>
</head>
<body>
  <div id="container">
    <h2>ESP32-CAM Last Photo</h2>
    <p>It might take more than 5 seconds to capture a photo.</p>
    <p>
      <button onclick="rotatePhoto();">ROTATE</button>
      <button onclick="capturePhoto()">CAPTURE PHOTO</button>
      <button onclick="location.reload();">REFRESH PAGE</button>
    </p>
  </div>
  <div><img src="saved-photo" id="photo" width="70%"></div>
</body>
<script>
  var deg = 0;
  function capturePhoto() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', "/capture", true);
    xhr.send();
  }
  function rotatePhoto() {
    var img = document.getElementById("photo");
    deg += 90;
    if(isOdd(deg/90)){ document.getElementById("container").className = "vert"; }
    else{ document.getElementById("container").className = "hori"; }
    img.style.transform = "rotate(" + deg + "deg)";
  }
  function isOdd(n) { return Math.abs(n % 2) == 1; }
</script>
</html>)rawliteral";

void setup() {
  // Serial port for debugging purposes
  Serial.begin(115200);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  if (!SPIFFS.begin(true)) {
    Serial.println("An Error has occurred while mounting SPIFFS");
    ESP.restart();
  }
  else {
    delay(500);
    Serial.println("SPIFFS mounted successfully");
  }

  // Print ESP32 Local IP Address
  Serial.print("IP Address: http://");
  Serial.println(WiFi.localIP());

  // Turn-off the 'brownout detector'
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);

  // OV2640 camera module
  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;

  if (psramFound()) {
    config.frame_size = FRAMESIZE_UXGA;
    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);
    ESP.restart();
  }

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(200, "text/html", index_html);
  });

  server.on("/capture", HTTP_GET, [](AsyncWebServerRequest * request) {
    takeNewPhoto = true;
    request->send(200, "text/plain", "Taking Photo");
  });

  server.on("/saved-photo", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(SPIFFS, FILE_PHOTO, "image/jpg", false);
  });

  // Start server
  server.begin();

}

void loop() {
  if (takeNewPhoto) {
    capturePhotoSaveSpiffs();
    takeNewPhoto = false;
  }
  delay(1);
}

// Check if photo capture was successful
bool checkPhoto( fs::FS &fs ) {
  File f_pic = fs.open( FILE_PHOTO );
  unsigned int pic_sz = f_pic.size();
  return ( pic_sz > 100 );
}

// Capture Photo and Save it to SPIFFS
void capturePhotoSaveSpiffs( void ) {
  camera_fb_t * fb = NULL; // pointer
  bool ok = 0; // Boolean indicating if the picture has been taken correctly

  do {
    // Take a photo with the camera
    Serial.println("Taking a photo...");

    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      return;
    }

    // Photo file name
    Serial.printf("Picture file name: %s\n", FILE_PHOTO);
    File file = SPIFFS.open(FILE_PHOTO, FILE_WRITE);

    // Insert the data in the photo file
    if (!file) {
      Serial.println("Failed to open file in writing mode");
    }
    else {
      file.write(fb->buf, fb->len); // payload (image), payload length
      Serial.print("The picture has been saved in ");
      Serial.print(FILE_PHOTO);
      Serial.print(" - Size: ");
      Serial.print(file.size());
      Serial.println(" bytes");
    }
    // Close the file
    file.close();
    esp_camera_fb_return(fb);

    // check if file has been correctly saved in SPIFFS
    ok = checkPhoto(SPIFFS);
  } while ( !ok );
}

Просмотреть исходный код

Как работает код

Сначала подключите необходимые библиотеки для работы с камерой, создания веб-сервера и использования SPIFFS.

#include "WiFi.h"
#include "esp_camera.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "soc/soc.h"           // Disable brownout problems
#include "soc/rtc_cntl_reg.h"  // Disable brownout problems
#include "driver/rtc_io.h"
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <FS.h>

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

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Создайте объект AsyncWebServer на порту 80.

AsyncWebServer server(80);

Булева переменная takeNewPhoto указывает, когда пора сделать новое фото.

boolean takeNewPhoto = false;

Затем определите путь и имя файла фото для сохранения в SPIFFS.

#define FILE_PHOTO "/photo.jpg"

Далее определите пины камеры для модуля ESP32-CAM 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

Создание веб-страницы

Далее у нас HTML для создания веб-страницы:

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { text-align:center; }
    .vert { margin-bottom: 10%; }
    .hori{ margin-bottom: 0%; }
  </style>
</head>
<body>
  <div id="container">
    <h2>ESP32-CAM Last Photo</h2>
    <p>It might take more than 5 seconds to capture a photo.</p>
    <p>
      <button onclick="rotatePhoto();">ROTATE</button>
      <button onclick="capturePhoto()">CAPTURE PHOTO</button>
      <button onclick="location.reload();">REFRESH PAGE</button>
    </p>
  </div>
  <div><img src="saved-photo" id="photo" width="70%"></div>
</body>
<script>
  var deg = 0;
  function capturePhoto() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', "/capture", true);
    xhr.send();
  }
  function rotatePhoto() {
    var img = document.getElementById("photo");
    deg += 90;
    if(isOdd(deg/90)){ document.getElementById("container").className = "vert"; }
    else{ document.getElementById("container").className = "hori"; }
    img.style.transform = "rotate(" + deg + "deg)";
  }
  function isOdd(n) { return Math.abs(n % 2) == 1; }
</script>
</html>)rawliteral";

Мы не будем вдаваться в детали работы этого HTML. Просто дадим краткий обзор.

По сути, создаются три кнопки: ROTATE, CAPTURE PHOTO и REFRESH PAGE. Каждая кнопка вызывает свою JavaScript-функцию: rotatePhoto(), capturePhoto() и reload().

<button onclick="rotatePhoto();">ROTATE</button>
<button onclick="capturePhoto()">CAPTURE PHOTO</button>
<button onclick="location.reload();">REFRESH PAGE</button>

Функция capturePhoto() отправляет запрос на URL /capture к ESP32, чтобы он сделал новое фото.

function capturePhoto() {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', "/capture", true);
  xhr.send();
}

Функция rotatePhoto() поворачивает фото.

function rotatePhoto() {
  var img = document.getElementById("photo");
  deg += 90;
  if(isOdd(deg/90)){ document.getElementById("container").className = "vert"; }
  else{ document.getElementById("container").className = "hori"; }
  img.style.transform = "rotate(" + deg + "deg)";
}
function isOdd(n) { return Math.abs(n % 2) == 1; }

Мы не уверены, какой «лучший» способ поворота фото с помощью JavaScript. Этот метод отлично работает, но, возможно, есть способы лучше. Если у вас есть предложения, поделитесь с нами.

Наконец, следующий раздел отображает фото.

<div><img src="saved-photo" id="photo" width="70%"></div>

При нажатии кнопки REFRESH загружается последнее изображение.

setup()

В setup() инициализируйте последовательную связь:

Serial.begin(115200);

Подключите ESP32-CAM к вашей локальной сети:

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Connecting to WiFi...");
}

Инициализируйте SPIFFS:

if (!SPIFFS.begin(true)) {
  Serial.println("An Error has occurred while mounting SPIFFS");
  ESP.restart();
}
else {
  delay(500);
  Serial.println("SPIFFS mounted successfully");
}

Выведите локальный IP-адрес ESP32-CAM:

Serial.print("IP Address: http://");
Serial.println(WiFi.localIP());

Следующие строки настраивают и инициализируют камеру с правильными параметрами.

Обработка веб-сервера

Далее нам нужно обработать, что происходит, когда ESP32-CAM получает запрос по определённому URL.

Когда ESP32-CAM получает запрос на корневой URL /, мы отправляем HTML-текст для построения веб-страницы.

server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
  request->send(200, "text/html", index_html);
});

Когда мы нажимаем кнопку «CAPTURE» на веб-сервере, отправляется запрос к ESP32 на URL /capture. При этом мы устанавливаем переменную takeNewPhoto в true, чтобы знать, что пора сделать новое фото.

server.on("/capture", HTTP_GET, [](AsyncWebServerRequest * request) {
  takeNewPhoto = true;
  request->send(200, "text/plain", "Taking Photo");
});

В случае запроса на URL /saved-photo, отправляем фото, сохранённое в SPIFFS, подключённому клиенту:

server.on("/saved-photo", HTTP_GET, [](AsyncWebServerRequest * request) {
  request->send(SPIFFS, FILE_PHOTO, "image/jpg", false);
});

Наконец, запускаем веб-сервер.

server.begin();

loop()

В loop(), если переменная takeNewPhoto равна True, мы вызываем capturePhotoSaveSpiffs() для съёмки нового фото и сохранения его в SPIFFS. Затем устанавливаем переменную takeNewPhoto в false.

void loop() {
  if (takeNewPhoto) {
    capturePhotoSaveSpiffs();
    takeNewPhoto = false;
  }
  delay(1);
}

Съёмка фото

В скетче есть ещё две функции: checkPhoto() и capturePhotoSaveSpiffs().

Функция checkPhoto() проверяет, было ли фото успешно сохранено в SPIFFS.

bool checkPhoto( fs::FS &fs ) {
  File f_pic = fs.open( FILE_PHOTO );
  unsigned int pic_sz = f_pic.size();
  return ( pic_sz > 100 );
}

Функция capturePhotoSaveSpiffs() делает фото и сохраняет его в SPIFFS.

void capturePhotoSaveSpiffs( void ) {
  camera_fb_t * fb = NULL; // pointer
  bool ok = 0; // Boolean indicating if the picture has been taken correctly

  do {
    // Take a photo with the camera
    Serial.println("Taking a photo...");

    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      return;
    }

    // Photo file name
    Serial.printf("Picture file name: %s\n", FILE_PHOTO);
    File file = SPIFFS.open(FILE_PHOTO, FILE_WRITE);

    // Insert the data in the photo file
    if (!file) {
      Serial.println("Failed to open file in writing mode");
    }
    else {
      file.write(fb->buf, fb->len); // payload (image), payload length
      Serial.print("The picture has been saved in ");
      Serial.print(FILE_PHOTO);
      Serial.print(" - Size: ");
      Serial.print(file.size());
      Serial.println(" bytes");
    }
    // Close the file
    file.close();
    esp_camera_fb_return(fb);

    // check if file has been correctly saved in SPIFFS
    ok = checkPhoto(SPIFFS);
  } while ( !ok );
}

Эта функция основана на скетче dualvim.

Загрузка кода в ESP32-CAM

Чтобы загрузить код в плату ESP32-CAM, подключите её к компьютеру с помощью FTDI-программатора. Следуйте приведённой ниже схеме подключения:

ESP32-CAM подключение к FTDI-программатору для загрузки программы через Arduino IDE

Важно: GPIO 0 должен быть подключён к GND, чтобы вы могли загрузить скетч.

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

ESP32-CAM

FTDI-программатор

GND

GND

5V

VCC (5V)

U0R

TX

U0T

RX

GPIO 0

GND

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

  1. Перейдите в Tools > Board и выберите AI-Thinker ESP32-CAM.

  2. Перейдите в Tools > Port и выберите COM-порт, к которому подключён ESP32.

  3. Затем нажмите кнопку загрузки для загрузки кода.

Кнопка загрузки Arduino IDE
  1. Когда вы начнёте видеть точки в окне отладки, как показано ниже, нажмите встроенную кнопку RST на ESP32-CAM.

Точки при загрузке кода

После нескольких секунд код должен быть успешно загружен на вашу плату.

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

Откройте браузер и введите IP-адрес ESP32-CAM. Затем нажмите «CAPTURE PHOTO», чтобы сделать новое фото, и подождите несколько секунд, пока фото сохранится в SPIFFS.

Затем, если вы нажмёте кнопку «REFRESH PAGE», страница обновится с последним сохранённым фото. Если вам нужно отрегулировать ориентацию изображения, вы всегда можете использовать кнопку «ROTATE».

ESP32-CAM веб-сервер — демонстрация отображения последнего фото

В окне Serial Monitor вашей Arduino IDE вы должны увидеть подобные сообщения:

ESP32-CAM веб-сервер Serial Monitor Arduino IDE

Устранение неисправностей

Если вы получаете какую-либо из следующих ошибок, прочитайте наше руководство Устранение неисправностей ESP32-CAM: исправление наиболее частых проблем (ссылка):

  • Failed to connect to ESP32: Timed out waiting for packet header

  • Camera init failed with error 0x20001 или аналогичная

  • Brownout detector или ошибка Guru meditation

  • Sketch too big error — выбрана неправильная схема разделов

  • Board at COMX is not available — COM-порт не выбран

  • Psram error: GPIO isr service is not installed

  • Слабый сигнал Wi-Fi

  • Нет IP-адреса в Serial Monitor Arduino IDE

  • Не удаётся открыть веб-сервер

  • Изображение запаздывает / высокая задержка

Заключение

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

Если вам нравится этот проект, вам также могут понравиться другие проекты с ESP32-CAM: