ESP32-CAM: съёмка фото и отображение на веб-сервере
Узнайте, как создать веб-сервер на плате ESP32-CAM, который позволяет отправить команду на съёмку фото и просмотреть последний сделанный снимок в браузере, сохранённый в SPIFFS. Мы также добавили возможность поворота изображения при необходимости.
У нас есть другие проекты с ESP32-CAM в блоге, которые могут вам понравиться. Фактически, вы можете развить этот проект, добавив PIR-датчик для съёмки фото при обнаружении движения, физическую кнопку для съёмки фото или включив возможности потокового видео по другому URL-пути.
Другие проекты и руководства по ESP32-CAM:
ESP32-CAM PIR-датчик движения с захватом фото (сохранение на microSD-карту)
ESP32-CAM веб-сервер потокового видео (Home Assistant, Node-RED и т.д.)
Смотрите видео-демонстрацию
Посмотрите следующее видео-демонстрацию, чтобы увидеть, что вы будете создавать в этом руководстве.
Необходимые компоненты
Для выполнения этого проекта вам понадобятся следующие компоненты:
ESP32-CAM с OV2640 (обзор платы) — читайте Лучшие платы ESP32-CAM
Провода «мама-мама» (jumper wires)
FTDI-программатор
Источник питания 5В или power bank
Обзор проекта
Следующее изображение показывает веб-сервер, который мы создадим в этом руководстве.
Когда вы откроете веб-сервер, вы увидите три кнопки:
ROTATE: в зависимости от ориентации вашего ESP32-CAM, вам может потребоваться повернуть фото;
CAPTURE PHOTO: при нажатии на эту кнопку ESP32-CAM делает новое фото и сохраняет его в SPIFFS ESP32. Пожалуйста, подождите не менее 5 секунд перед обновлением веб-страницы, чтобы убедиться, что ESP32-CAM сделал и сохранил фото;
REFRESH PAGE: при нажатии на эту кнопку веб-страница обновляется и показывает последнее фото.
Примечание
Как упоминалось ранее, последнее сделанное фото хранится в SPIFFS ESP32, поэтому даже при перезагрузке платы вы всегда можете получить доступ к последнему сохранённому фото.
Установка дополнения ESP32
Мы будем программировать плату ESP32 с помощью Arduino IDE. Поэтому вам нужна установленная Arduino IDE, а также дополнение ESP32:
Установка библиотек
Мы построим веб-сервер с использованием следующих библиотек:
ESPAsyncWebServer от ESP32Async
AsyncTCP от ESP32Async
Вы можете установить эти библиотеки через Arduino Library Manager. Откройте Library Manager, нажав на значок библиотеки в левой боковой панели.
Найдите ESPAsyncWebServer и установите ESPAsyncWebServer от ESP32Async.
Затем установите библиотеку AsyncTCP. Найдите AsyncTCP и установите AsyncTCP от ESP32Async.
Скетч веб-сервера 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-программатора. Следуйте приведённой ниже схеме подключения:
Важно: GPIO 0 должен быть подключён к GND, чтобы вы могли загрузить скетч.
Многие FTDI-программаторы имеют перемычку, позволяющую выбрать 3.3В или 5В. Убедитесь, что перемычка установлена в правильное положение для выбора 5В.
ESP32-CAM |
FTDI-программатор |
|---|---|
GND |
GND |
5V |
VCC (5V) |
U0R |
TX |
U0T |
RX |
GPIO 0 |
GND |
Для загрузки кода выполните следующие шаги:
Перейдите в Tools > Board и выберите AI-Thinker ESP32-CAM.
Перейдите в Tools > Port и выберите COM-порт, к которому подключён ESP32.
Затем нажмите кнопку загрузки для загрузки кода.
Когда вы начнёте видеть точки в окне отладки, как показано ниже, нажмите встроенную кнопку RST на ESP32-CAM.
После нескольких секунд код должен быть успешно загружен на вашу плату.
Демонстрация
Откройте браузер и введите IP-адрес ESP32-CAM. Затем нажмите «CAPTURE PHOTO», чтобы сделать новое фото, и подождите несколько секунд, пока фото сохранится в SPIFFS.
Затем, если вы нажмёте кнопку «REFRESH PAGE», страница обновится с последним сохранённым фото. Если вам нужно отрегулировать ориентацию изображения, вы всегда можете использовать кнопку «ROTATE».
В окне 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: