ESP32-CAM: сохранение фотографий в Firebase Storage

В этом руководстве вы узнаете, как делать и загружать фотографии в Firebase Storage с помощью ESP32-CAM. Вы создадите проект Firebase с Storage, который позволяет хранить ваши файлы. Затем вы сможете перейти в консоль Firebase для просмотра фотографий или создать веб-приложение для их отображения (мы сделаем это в будущем уроке). ESP32-CAM будет запрограммирован с использованием Arduino IDE.

ESP32-CAM сохранение фотографий в Firebase Storage

Примечание

Этот проект совместим с любой платой ESP32 Camera с камерой OV2640. Вам просто нужно убедиться, что вы используете правильную распиновку для вашей платы.

Что такое Firebase?

Логотип Firebase

Firebase — это платформа разработки мобильных приложений от Google, которая помогает создавать, улучшать и развивать ваше приложение. Она имеет множество сервисов для управления данными из любого Android-, iOS- или веб-приложения, таких как аутентификация, база данных реального времени, хостинг, хранилище и т.д.

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

Этот простой урок демонстрирует, как делать и отправлять фотографии, сделанные ESP32-CAM, в Firebase Storage. ESP32-CAM делает снимок и отправляет его в Firebase каждый раз при перезагрузке (нажатие кнопки RST). Идея в том, чтобы добавить какой-либо триггер, который может быть полезен для ваших проектов, например PIR-датчик движения или кнопку.

ESP32-CAM Firebase Storage обзор проекта
  • Когда ESP32 запускается впервые, он делает новый снимок и сохраняет его в файловой системе (LittleFS);

  • ESP32-CAM подключается к Firebase как пользователь с email и паролем;

  • ESP32-CAM отправляет фотографию в Firebase Storage;

  • После этого вы можете перейти в консоль Firebase для просмотра фотографий;

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

Вот краткое описание шагов, которые необходимо выполнить для создания этого проекта:

  1. Создание проекта Firebase

  2. Настройка методов аутентификации

  3. Создание Storage Bucket

  4. Получение API-ключа проекта

  5. ESP32-CAM — отправка фотографий в Firebase Storage

1) Создание проекта Firebase

Следуйте приведённым ниже инструкциям для создания нового проекта в Firebase.

  1. Перейдите на Firebase и войдите с помощью аккаунта Google.

  2. Перейдите в Firebase Console и создайте новый проект.

  3. Дайте имя вашему проекту, например: ESP-Project, и нажмите Continue.

Создание проекта Firebase для ESP32 — шаг 1
  1. Далее включите или отключите AI-помощника для вашего проекта. Это необязательно.

Создание проекта Firebase — AI помощник
  1. Отключите опцию Enable Google Analytics для этого проекта, так как она не нужна. Затем нажмите Create project.

Отключение Google Analytics для проекта Firebase
  1. Настройка проекта займёт несколько секунд. Нажмите Continue, когда он будет готов.

Проект Firebase для ESP32 готов
  1. Вы будете перенаправлены на страницу консоли вашего проекта.

Консоль проекта Firebase

2) Настройка методов аутентификации

Вам нужно настроить методы аутентификации для вашего приложения.

«Большинство приложений должны знать личность пользователя. Другими словами, это обеспечивает вход в систему и идентификацию пользователей (в данном случае ESP32). Знание личности пользователя позволяет приложению безопасно сохранять пользовательские данные в облаке…» Чтобы узнать больше о методах аутентификации, вы можете прочитать документацию.

  1. На левой боковой панели нажмите Build > Authentication, а затем Get started.

Настройка аутентификации в проекте Firebase
  1. Существует несколько методов аутентификации, таких как email и пароль, аккаунт Google, аккаунт Facebook и другие.

Методы аутентификации Firebase
  1. Выберите Email/Password и включите этот метод аутентификации. Затем нажмите Save.

Включение входа по Email/Password в Firebase
  1. Затем вверху нажмите на вкладку Users. Затем нажмите Add user.

Создание нового пользователя Firebase
  1. Создайте нового пользователя с email и паролем. Email может быть вашим личным. Создайте пароль для этого пользователя (вам нужно будет помнить пароль позже). Наконец, нажмите Add user.

Добавление пользователя с email и паролем в Firebase
  1. Пользователь появится в списке пользователей. Вы можете увидеть информацию о пользователе, например, когда он был создан, когда последний раз входил в систему и его UID.

Пользователь создан в Firebase

3) Создание Storage Bucket

1) На левой боковой панели нажмите Build > Storage, а затем Get started.

Консоль Firebase — создание Storage Bucket

Для использования Firebase Storage вам нужно обновить проект до платного плана. Но не волнуйтесь — они предлагают 5 ГБ бесплатного хранилища, чего более чем достаточно для запуска этого и многих других проектов в течение долгого времени без оплаты. Кроме того, вы можете установить максимальный лимит расходов.

2) Создайте платёжный аккаунт для привязки к вашему проекту.

Firebase Storage — создание платёжного аккаунта

Установите тарифный план Pay as you go Blaze.

Firebase Pay as You Go тариф Blaze

3) После создания и настройки вашего платёжного аккаунта и привязки его к проекту, вернитесь в консоль проекта для создания Storage Bucket.

Firebase Storage — начало работы

4) Выберите расположение базы данных.

Настройка Default Storage Bucket в Firebase

5) Выберите Start in **test mode** — нажмите Next. Мы изменим правила хранилища позже.

Firebase Storage Bucket — запуск в тестовом режиме

6) Storage bucket теперь настроен. Скопируйте ID storage bucket — он понадобится вам позже. Не копируйте часть gs://.

Firebase Storage Bucket создан

Правила хранилища

Мы изменим правила хранилища, чтобы только аутентифицированные пользователи могли загружать файлы в storage bucket. Выберите вкладку Rules.

Firebase Storage Bucket — изменение правил

Измените правила вашей базы данных. Используйте следующие правила:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth !=null;
    }
  }
}

Когда закончите, нажмите Publish.

4) Получение API-ключа проекта

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

  1. Чтобы получить API-ключ проекта, на левой боковой панели нажмите Project Settings.

Настройки проекта Firebase
  1. Скопируйте API Key в безопасное место, так как он понадобится вам позже.

API-ключ проекта Firebase

Теперь у вас есть всё необходимое для взаимодействия ESP32 с базой данных.

5) ESP32-CAM — отправка фотографий в Firebase Storage

Перед тем как продолжить урок, убедитесь, что выполнены следующие предварительные требования.

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

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

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

Для этого урока вам нужно установить библиотеку FirebaseClient.

Установка — Arduino IDE

Следуйте этому разделу, если вы используете Arduino IDE.

Перейдите в Sketch > Include Library > Manage Libraries, найдите библиотеку по имени и установите её.

Установка библиотеки Firebase Client в Arduino IDE

Теперь вы готовы начать программирование плат ESP32 и ESP8266 для взаимодействия с базой данных.

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

Следуйте приведённым ниже инструкциям, если вы используете VS Code с расширением PlatformIO или pioarduino.

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

Нажмите на значок PIO Home и выберите вкладку Libraries. Найдите «FirebaseClient». Выберите библиотеку FirebaseClient от Mobitz.

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

Затем нажмите Add to Project и выберите проект, над которым вы работаете.

Добавление библиотеки FirebaseClient в проект в VS Code

Также измените скорость монитора на 115200, добавив следующую строку в файл platformio.ini вашего проекта:

monitor_speed = 115200

Теперь вы готовы начать программирование платы ESP32-CAM для отправки фотографий в Firebase Storage.

ESP32-CAM — отправка фотографий в Firebase — Код

Скопируйте следующий код в Arduino IDE или в файл main.cpp, если вы используете VS Code. Он делает снимок и отправляет его в Firebase при первой загрузке.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete instructions at: https://RandomNerdTutorials.com/esp32-cam-save-picture-firebase-storage/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  and associated documentation files. The above copyright notice and this permission notice
  shall be included in all copies or substantial portions of the Software.
  Based on the example provided by the ESP Firebase Client Library
*********/
#define ENABLE_USER_AUTH
#define ENABLE_STORAGE
#define ENABLE_FS

#include <Arduino.h>
#include <FirebaseClient.h>
#include <FS.h>
#include <LittleFS.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"

#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"

#define API_KEY "REPLACE_WITH_YOUR_FIREBASE_PROJECT_API_KEY"
#define USER_EMAIL "REPLACE_WITH_FIREBASE_PROJECT_EMAIL_USER"
#define USER_PASSWORD "REPLACE_WITH_FIREBASE_PROJECT_USER_PASS"

// Define the Firebase storage bucket ID e.g bucket-name.appspot.com */
#define STORAGE_BUCKET_ID "REPLACE_WITH_STORAGE_BUCKET_ID"

// Photo path in filesystem and photo path in Firebase bucket
#define FILE_PHOTO_PATH "/photo.jpg"
#define BUCKET_PHOTO_PATH "/data/photo.jpg"

// User functions
void processData(AsyncResult &aResult);
void file_operation_callback(File &file, const char *filename, file_operating_mode mode);

FileConfig media_file(FILE_PHOTO_PATH, file_operation_callback); // Can be set later with media_file.setFile("/image.png", file_operation_callback);

File myFile;

// Authentication
UserAuth user_auth(API_KEY, USER_EMAIL, USER_PASSWORD, 3000 /* expire period in seconds (<3600) */);

// Firebase components
FirebaseApp app;
WiFiClientSecure ssl_client;
using AsyncClient = AsyncClientClass;
AsyncClient aClient(ssl_client);
Storage storage;

bool taskComplete = false;

AsyncResult storageResult;

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

boolean takeNewPhoto = true;

// Capture Photo and Save it to LittleFS
void capturePhotoSaveLittleFS( void ) {
  // Dispose first pictures because of bad quality
  camera_fb_t* fb = NULL;
  // Skip first 3 frames (increase/decrease number as needed).
  for (int i = 0; i < 10; i++) {
    fb = esp_camera_fb_get();
    esp_camera_fb_return(fb);
    fb = NULL;
  }

  // Take a new photo
  fb = NULL;
  fb = esp_camera_fb_get();
  if(!fb) {
    Serial.println("Camera capture failed");
    delay(1000);
    ESP.restart();
  }

  // Photo file name
  Serial.printf("Picture file name: %s\n", FILE_PHOTO_PATH);
  File file = LittleFS.open(FILE_PHOTO_PATH, 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_PATH);
    Serial.print(" - Size: ");
    Serial.print(fb->len);
    Serial.println(" bytes");
  }
  // Close the file
  file.close();
  esp_camera_fb_return(fb);
  delay(100);
}

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

void initWiFi(){
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
}

void initCamera(){
 // 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;
  config.grab_mode = CAMERA_GRAB_LATEST;

  if (psramFound()) {
    config.frame_size = FRAMESIZE_UXGA;
    config.jpeg_quality = 10;
    config.fb_count = 1;
  } 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();
  }
  Serial.print("Camera init success");
}

void setup(){
    Serial.begin(115200);
    initWiFi();
    initCamera();

    Firebase.printf("Firebase Client v%s\n", FIREBASE_CLIENT_VERSION);

    initLittleFS();

    // Configure SSL client
    ssl_client.setInsecure();
    ssl_client.setConnectionTimeout(1000);
    ssl_client.setHandshakeTimeout(5);
    Serial.println("Initializing app...");
    initializeApp(aClient, app, getAuth(user_auth), processData, "authTask");

    app.getApp<Storage>(storage);

    Serial.println("Listing files in LittleFS:");
    File root = LittleFS.open("/");
    File file = root.openNextFile();
    while (file) {
        Serial.println(file.name());
        file = root.openNextFile();
    }

}

void loop(){
    // To maintain the authentication process.
    app.loop();

    if (app.ready() && !taskComplete && takeNewPhoto){
        taskComplete = true;
        takeNewPhoto = false;

        capturePhotoSaveLittleFS();

        // Async call with callback function.
        storage.upload(aClient, FirebaseStorage::Parent(STORAGE_BUCKET_ID, BUCKET_PHOTO_PATH), getFile(media_file), "image/jpg", processData, "uploadTask");
    }
}

void processData(AsyncResult &aResult)
{
    // Exits when no result available when calling from the loop.
    if (!aResult.isResult())
        return;

    if (aResult.isEvent())
    {
        Firebase.printf("Event task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.appEvent().message().c_str(), aResult.appEvent().code());
    }

    if (aResult.isDebug())
    {
        Firebase.printf("Debug task: %s, msg: %s\n", aResult.uid().c_str(), aResult.debug().c_str());
    }

    if (aResult.isError())
    {
        Firebase.printf("Error task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.error().message().c_str(), aResult.error().code());
    }

    if (aResult.downloadProgress())
    {
        Firebase.printf("Downloaded, task: %s, %d%s (%d of %d)\n", aResult.uid().c_str(), aResult.downloadInfo().progress, "%", aResult.downloadInfo().downloaded, aResult.downloadInfo().total);
        if (aResult.downloadInfo().total == aResult.downloadInfo().downloaded)
        {
            Firebase.printf("Download task: %s, complete!\n", aResult.uid().c_str());
        }
    }

    if (aResult.uploadProgress())
    {
        Firebase.printf("Uploaded, task: %s, %d%s (%d of %d)\n", aResult.uid().c_str(), aResult.uploadInfo().progress, "%", aResult.uploadInfo().uploaded, aResult.uploadInfo().total);
        if (aResult.uploadInfo().total == aResult.uploadInfo().uploaded)
        {
            Firebase.printf("Upload task: %s, complete!\n", aResult.uid().c_str());
            Serial.print("Download URL: ");
            Serial.println(aResult.uploadInfo().downloadUrl);
        }
    }
}

void file_operation_callback(File &file, const char *filename, file_operating_mode mode){
    // FILE_OPEN_MODE_READ, FILE_OPEN_MODE_WRITE and FILE_OPEN_MODE_APPEND are defined in this library
    switch (mode)    {
    case file_mode_open_read:
        myFile = LittleFS.open(filename, "r");
        if (!myFile || !myFile.available()) {
            Serial.println("[ERROR] Failed to open file for reading");
        }
        break;
    case file_mode_open_write:
        myFile = LittleFS.open(filename, "w");
        break;
    case file_mode_open_append:
        myFile = LittleFS.open(filename, "a");
        break;
    case file_mode_remove:
        LittleFS.remove(filename);
        break;
    default:
        break;
    }
    // Set the internal FS object with global File object.
    file = myFile;
}

Исходный код на GitHub

Вам нужно вставить свои сетевые данные, email и пароль пользователя Firebase, URL storage bucket и API-ключ проекта, чтобы проект заработал.

Этот скетч основан на базовом примере, предоставленном библиотекой. Вы можете найти больше примеров здесь.

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

Продолжайте чтение, чтобы узнать, как работает код, или перейдите к разделу демонстрации.

Библиотеки

Сначала подключите необходимые библиотеки.

#include <Arduino.h>
#include <FirebaseClient.h>
#include <FS.h>
#include <LittleFS.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"

Сетевые данные

Вставьте свои сетевые данные в следующие переменные, чтобы ESP мог подключиться к интернету и взаимодействовать с Firebase.

//Replace with your network credentials
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"

API-ключ проекта Firebase

Вставьте API-ключ вашего проекта Firebase — смотрите раздел: 4) Получение API-ключа проекта.

#define API_KEY "REPLACE_WITH_YOUR_FIREBASE_PROJECT_API_KEY"

Email и пароль пользователя

Вставьте авторизованный email и соответствующий пароль — смотрите раздел: 2) Настройка методов аутентификации.

#define USER_EMAIL "REPLACE_WITH_FIREBASE_PROJECT_EMAIL_USER"
#define USER_PASSWORD "REPLACE_WITH_FIREBASE_PROJECT_USER_PASS"

ID Firebase Storage Bucket

Вставьте ID Firebase storage bucket, например bucket-name.appspot.com. В моём случае это esp-project-598f5.firebasestorage.app. (Удалите все слэши «/» в конце или в начале bucket ID, иначе это не будет работать).

#define STORAGE_BUCKET_ID "REPLACE_WITH_YOUR_STORAGE_BUCKET_ID"

Путь к фотографии

Переменная FILE_PHOTO_PATH определяет путь в LittleFS, где будет сохранена фотография. Она будет сохранена с именем photo.jpg.

#define FILE_PHOTO "/photo.jpg"

У нас также есть переменная для хранения пути, по которому фотография будет сохранена в Storage Bucket в Firebase.

#define BUCKET_PHOTO "/data/photo.jpg"

Настройка файла

Создайте объект FileConfig для использования с функциями Firebase. Он принимает путь к файлу в bucket и callback-функцию для получения файла (будь то из файловой системы или SD-карты, например).

FileConfig media_file(FILE_PHOTO_PATH, file_operation_callback); // Can be set later with media_file.setFile("/image.png", file_operation_callback);

Наша callback-функция file_operation_callback() получает файл из LittleFS.

void file_operation_callback(File &file, const char *filename, file_operating_mode mode){
    switch (mode)    {
    case file_mode_open_read:
        myFile = LittleFS.open(filename, "r");
        if (!myFile || !myFile.available()) {
            Serial.println("[ERROR] Failed to open file for reading");
        }
        break;
    case file_mode_open_write:
        myFile = LittleFS.open(filename, "w");
        break;
    case file_mode_open_append:
        myFile = LittleFS.open(filename, "a");
        break;
    case file_mode_remove:
        LittleFS.remove(filename);
        break;
    default:
        break;
    }
    // Set the internal FS object with global File object.
    file = myFile;
}

Создайте глобальную переменную типа File с именем myFile, которая будет использоваться на протяжении всего кода для обращения к файлу, который мы хотим загрузить в файловую систему.

File myFile;

Объявление аутентификации и компонентов Firebase

Следующая строка создаёт объект аутентификации, используя API-ключ проекта, email пользователя проекта и пароль.

UserAuth user_auth(API_KEY, USER_EMAIL, USER_PASSWORD, 3000 /* expire period in seconds (<3600) */);

Это создаёт экземпляр FirebaseApp с именем app, который ссылается на приложение Firebase.

FirebaseApp app;

Следующие строки настраивают фреймворк асинхронного взаимодействия с Firebase. По сути, вы создаёте SSL-клиент с использованием библиотеки WiFiClientSecure. Затем вы создаёте экземпляр асинхронного клиента aClient, который обеспечивает безопасный HTTPS. Это позволит вам обрабатывать сетевые операции асинхронно.

WiFiClientSecure ssl_client;
using AsyncClient = AsyncClientClass;
AsyncClient aClient(ssl_client);

Следующая строка создаёт объект Storage с именем storage, который представляет Firebase storage bucket.

Storage storage;

Определение пинов ESP32-CAM

Следующие строки определяют пины ESP32-CAM. Это определение для модуля ESP32-CAM AI-Thinker. Если вы используете другой модуль ESP32-CAM, вам нужно изменить определение пинов — ознакомьтесь с этим руководством: ESP32-CAM Camera Boards: Pin and GPIOs Assignment Guide.

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

Другие переменные

Переменная takeNewPhoto проверяет, пора ли делать новый снимок. Мы установим её в true, чтобы снимок был сделан при первом запуске платы.

boolean takeNewPhoto = true;

Переменная taskCompleted — это булева переменная, которая проверяет, успешно ли мы подключились к Firebase.

bool taskCompleted = false;

Функция capturePhotoSaveLittleFS()

Функция capturePhotoSaveLittleFS() делает снимок и сохраняет его в файловой системе ESP32.

// Capture Photo and Save it to LittleFS
void capturePhotoSaveLittleFS( void ) {
  // Dispose first pictures because of bad quality
  camera_fb_t* fb = NULL;
  // Skip first 3 frames (increase/decrease number as needed).
  for (int i = 0; i < 4; i++) {
    fb = esp_camera_fb_get();
    esp_camera_fb_return(fb);
    fb = NULL;
  }

  // Take a new photo
  fb = NULL;
  fb = esp_camera_fb_get();
  if(!fb) {
    Serial.println("Camera capture failed");
    delay(1000);
    ESP.restart();
  }

  // Photo file name
  Serial.printf("Picture file name: %s\n", FILE_PHOTO_PATH);
  File file = LittleFS.open(FILE_PHOTO_PATH, 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_PATH);
    Serial.print(" - Size: ");
    Serial.print(fb->len);
    Serial.println(" bytes");
  }
  // Close the file
  file.close();
  esp_camera_fb_return(fb);
}

Функция initWiFi()

Функция initWiFi() инициализирует Wi-Fi.

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

Функция initLittleFS()

Функция initLittleFS() инициализирует файловую систему LittleFS.

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

Функция initCamera()

Функция initCamera() инициализирует ESP32-CAM.

void initCamera(){
 // 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_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_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();
  }
}

setup()

В setup() инициализируются Serial Monitor, Wi-Fi, LittleFS и камера.

void setup(){
  Serial.begin(115200);
  initWiFi();
  initCamera();

  Firebase.printf("Firebase Client v%s\n", FIREBASE_CLIENT_VERSION);

  initLittleFS();

Настройка SSL-клиента.

ssl_client.setInsecure();
ssl_client.setConnectionTimeout(1000);
ssl_client.setHandshakeTimeout(5);

Следующая строка инициализирует приложение Firebase с аутентификацией и устанавливает processData() как callback-функцию для асинхронных результатов (это означает, что любые результаты из функции initializeApp() будут обрабатываться в callback-функции processData()).

initializeApp(aClient, app, getAuth(user_auth), processData, "authTask");

Затем укажите, что вы хотите установить объект Storage, определённый ранее, в качестве хранилища для нашего приложения Firebase.

app.getApp<Storage>(storage);

Проверка и вывод списка файлов, хранящихся в файловой системе LittleFS (этот шаг необязателен).

Serial.println("Listing files in LittleFS:");
File root = LittleFS.open("/");
File file = root.openNextFile();
while (file) {
    Serial.println(file.name());
    file = root.openNextFile();
}

loop()

Библиотека Firebase, которую мы используем, работает асинхронно и с callback-функциями. Это означает, что когда происходит событие, запускаются соответствующие назначенные callback-функции. Чтобы поддерживать работу приложения Firebase, обработку аутентификации и асинхронных задач, нам нужно добавить app.loop() в начало нашей функции loop().

// To maintain the authentication process.
app.loop();

Команда app.ready() проверяет, завершена ли аутентификация Firebase и готова ли она, мы также проверяем, пора ли делать новый снимок.

if (app.ready() && !taskComplete && takeNewPhoto){

Если да, мы сбрасываем переменные taskComplete и takeNewPhoto в их исходные состояния.

taskComplete = true;
takeNewPhoto = false;

Мы вызываем функцию capturePhotoSaveLittleFS() для съёмки фотографии ESP32-CAM и сохранения её в файловой системе ESP32.

capturePhotoSaveLittleFS();

Наконец, отправляем фотографию в Firebase с помощью функции upload() объекта storage.

storage.upload(aClient, FirebaseStorage::Parent(STORAGE_BUCKET_ID, BUCKET_PHOTO_PATH), getFile(media_file), "image/jpg", processData, "uploadTask");

Давайте рассмотрим каждый аргумент этой функции:

  • aClient: это объект асинхронного клиента Firebase, используемый для подключения к Firebase.

  • FirebaseStorage::Parent(STORAGE_BUCKET_ID, BUCKET_PHOTO_PATH): указывает расположение в Firebase Storage, куда будет загружен файл:

    • STORAGE_BUCKET_ID: уникальный ID Firebase Storage bucket

    • BUCKET_PHOTO_PATH: путь или папка внутри bucket, где будет храниться файл

  • getFile(media_file): загружаемый файл, полученный с помощью функции getFile(). Мы определили файл ранее в начале кода.

  • «image/jpg»: MIME-тип файла, указывающий его формат. Здесь указано, что файл является JPEG-изображением.

  • processData: callback-функция, которая обрабатывает результат операции загрузки.

  • «uploadTask»: уникальный идентификатор для идентификации этой конкретной задачи позже в функции processData().

Функция processData — обработка задач Firebase

Функция processData() будет обрабатывать все события, связанные с операциями Firebase, включая загрузку файлов в Firebase storage.

void processData(AsyncResult &aResult)
{
    // Exits when no result available when calling from the loop.
    if (!aResult.isResult())
        return;

    if (aResult.isEvent())
    {
        Firebase.printf("Event task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.appEvent().message().c_str(), aResult.appEvent().code());
    }

    if (aResult.isDebug())
    {
        Firebase.printf("Debug task: %s, msg: %s\n", aResult.uid().c_str(), aResult.debug().c_str());
    }

    if (aResult.isError())
    {
        Firebase.printf("Error task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.error().message().c_str(), aResult.error().code());
    }

    if (aResult.downloadProgress())
    {
        Firebase.printf("Downloaded, task: %s, %d%s (%d of %d)\n", aResult.uid().c_str(), aResult.downloadInfo().progress, "%", aResult.downloadInfo().downloaded, aResult.downloadInfo().total);
        if (aResult.downloadInfo().total == aResult.downloadInfo().downloaded)
        {
            Firebase.printf("Download task: %s, complete!\n", aResult.uid().c_str());
        }
    }

    if (aResult.uploadProgress())
    {
        Firebase.printf("Uploaded, task: %s, %d%s (%d of %d)\n", aResult.uid().c_str(), aResult.uploadInfo().progress, "%", aResult.uploadInfo().uploaded, aResult.uploadInfo().total);
        if (aResult.uploadInfo().total == aResult.uploadInfo().uploaded)
        {
            Firebase.printf("Upload task: %s, complete!\n", aResult.uid().c_str());
            Serial.print("Download URL: ");
            Serial.println(aResult.uploadInfo().downloadUrl);
        }
    }
}

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

После ввода необходимых данных загрузите код на ваш ESP32-CAM. Если вы не знаете, как загрузить код на ESP32-CAM, вы можете следовать следующим руководствам:

После загрузки кода откройте Serial Monitor на скорости 115200 бод. Нажмите встроенную кнопку RST на ESP32-CAM.

Он войдёт в Firebase, сделает снимок и сохранит его в LittleFS. После этого загрузит фотографию в Firebase Storage.

ESP32-CAM загрузка фотографии в Firebase — Serial Monitor

Теперь перейдите в консоль Firebase и выберите вкладку Storage. Там должна быть папка data, содержащая ваше фото.

Папка создана в Firebase Storage

Вы можете просмотреть метаданные фотографии и открыть её в полном размере. Вы также можете получить доступ к изображению по Download URL, напечатанному в Serial Monitor.

Фотография загружена в Firebase Storage — метаданные

Заключение

В этом уроке вы узнали, как создать проект Firebase с Storage. Firebase Storage позволяет хранить файлы в облаке. Затем вы можете получить доступ к этим файлам, перейдя в консоль Firebase, или вы можете создать веб-приложение для отображения этих файлов (ознакомьтесь с этим уроком: ESP32-CAM: отображение фотографий в веб-приложении Firebase).

Мы показали простой пример отправки фотографии, сделанной ESP32-CAM, в Firebase Storage. Пример максимально прост, чтобы вы могли понять основы. Идея в том, чтобы модифицировать проект для создания чего-то полезного — например, делать снимок и загружать его в Firebase Storage при обнаружении движения, при открытии двери или при нажатии кнопки.

У нас есть другие уроки по Firebase, которые могут вам понравиться: