ESP32-CAM: считыватель QR-кодов с веб-сервером управления пользователями

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

ESP32-CAM считыватель QR-кодов — система управления пользователями (веб-сервер)

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

На следующей схеме показан общий обзор работы проекта.

ESP32-CAM QR Code Management Web Server

1) Когда ESP32-CAM обнаруживает действительный QR-код, она считывает его данные и встроенный светодиод-вспышка мигает.

2) ESP32-CAM записывает время этого взаимодействия и сохраняет время и данные QR-кода на карту microSD в файл с именем log.txt.

3) ESP32-CAM также размещает веб-сервер для отображения и управления информацией с карты microSD.

4) Корневой URL (/) показывает полный журнал (сохранённый на карте microSD в файле log.txt) с временной меткой и данными QR-кода.

5) Есть ещё одна страница по пути /add-user, которая позволяет добавлять пользователей и их роли с помощью формы.

6) Данные, введённые через эту форму, будут сохранены в файле user.txt на карте microSD.

7) Ещё одна страница по пути /manage-users позволяет просматривать и удалять пользователей.

8) Эта страница позволяет взаимодействовать с файлом users.txt.

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

Мы будем использовать плату ESP32-CAM с обозначением AI-Thinker, но другие модули тоже должны работать при правильном назначении выводов в коде. Плата ESP32-CAM — это устройство стоимостью около $9 (или меньше), которое объединяет чип ESP32-S, камеру OV2640, слот для карты microSD и несколько выводов GPIO.

Плата ESP32-CAM с камерой OV2640, слотом для microSD карты и GPIO выводами

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

Если вы используете плату ESP32 с камерой без поддержки microSD карты, вам также понадобится модуль microSD карты.

Для ознакомления с ESP32-CAM вы можете изучить следующие уроки:

Подготовка Arduino IDE

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

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

Для этого урока мы будем использовать библиотеку ESP32QRCodeReader от Alvarowolfx, которая упрощает считывание QR-кодов с платы ESP32-CAM.

  • Нажмите здесь, чтобы скачать библиотеку ESP32QRCodeReader: Скачать

Затем в Arduino IDE перейдите в Sketch > Include Library > Add .ZIP library и установите только что скачанную библиотеку.

Arduino IDE — установка библиотеки из ZIP-архива

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

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

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

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

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

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

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

Плагин загрузчика LittleFS

Файлы, используемые для создания веб-сервера, будут сохранены в файловой системе LittleFS на ESP32. Убедитесь, что у вас установлен плагин LittleFS Uploader в Arduino IDE. Вы можете следовать этому руководству:

Подготовка карты MicroSD

Перед продолжением убедитесь, что вы отформатировали карту microSD в формате FAT32. Следуйте приведённым ниже инструкциям для форматирования карты microSD или используйте программу SD Card Formatter (совместима с Windows и Mac OS).

1. Вставьте карту microSD в компьютер. Перейдите в Мой компьютер и щёлкните правой кнопкой мыши по SD-карте. Выберите Форматирование, как показано на рисунке ниже.

Форматирование MicroSD карты

2. Появится новое окно. Выберите FAT32, нажмите Начать, чтобы инициализировать процесс форматирования, и следуйте инструкциям на экране.

Форматирование MicroSD карты в FAT32

Подключите карту microSD к ESP32-CAM:

ESP32-CAM — подключение карты Micro SD в слот

Чтобы узнать, как использовать карту microSD с ESP32-CAM, вы можете прочитать следующий урок:

Организация файлов

Для удобства организации проекта мы создадим 6 файлов для веб-сервера:

  • Скетч Arduino: для обработки веб-сервера, считывателя QR-кодов и карты microSD;

  • full-log.html: загружает журнал всех успешно сканированных QR-кодов и данных пользователей;

  • manage-users.html: веб-страница, позволяющая просматривать и удалять пользователей;

  • add-user.html: веб-страница, позволяющая добавлять новых пользователей с уникальным QR-кодом;

  • get.html: обрабатывает все HTTP GET-запросы;

  • style.css: для стилизации веб-страницы.

Структура файлов скетча Arduino

HTML- и CSS-файлы необходимо сохранить в папке data внутри папки скетча Arduino, как показано на предыдущей схеме. Мы загрузим эти файлы в файловую систему ESP32-CAM (LittleFS).

Вы можете скачать все файлы проекта:

HTML-файлы

Скопируйте следующий код в файл full-log.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Manage Users</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <div class="nav-container">
            <a href="/" class="brand">User Management</a>
            <ul class="nav-menu">
                <li><a href="/">📄 Full Log</a></li>
                <li><a href="add-user">➕ Add User</a></li>
                <li><a href="manage-users">👤 Manage Users</a></li>
            </ul>
        </div>
    </nav>
    <div class="main-container">
        <section class="main-section">
            <h2>📄 Full Access Log</h2>
            <table id="tableData">
                <thead>
                    <tr>
                        <th>Date</th>
                        <th>Time</th>
                        <th>QR Code</th>
                        <th>Role</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- Data from log.txt will be loaded here -->
                </tbody>
            </table>
        </section>
    </div>
    <div class="main-container">
        <a href="get?delete=log"><button class="button button-delete">🗑️ Delete log.txt File</button></a>
    </div>
    <script>
        // JavaScript to load and parse log.txt
        async function loadTableData() {
            try {
                const response = await fetch('view-log');
                const data = await response.text();
                const rows = data.trim().split('\n').slice(1); // Skip the header line

                const tableBody = document.querySelector('#tableData tbody');
                rows.forEach(row => {
                    const columns = row.split(',');
                    const tr = document.createElement('tr');
                    columns.forEach(column => {
                        const td = document.createElement('td');
                        td.textContent = column;
                        tr.appendChild(td);
                    });
                    tableBody.appendChild(tr);
                });
            } catch (error) {
                console.error('Error loading log data:', error);
            }
        }
        // Call the function to load log data
        loadTableData();
    </script>
</body>
</html>

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

Скопируйте следующий код в файл manage-users.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Manage Users</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <div class="nav-container">
            <a href="/" class="brand">User Management</a>
            <ul class="nav-menu">
                <li><a href="/">📄 Full Log</a></li>
                <li><a href="add-user">➕ Add User</a></li>
                <li><a href="manage-users">👤 Manage Users</a></li>
            </ul>
        </div>
    </nav>
    <div class="main-container">
        <section class="main-section">
            <h2>👤 User Log</h2>
            <table id="tableData">
                <thead>
                    <tr>
                        <th>QR Code</th>
                        <th>Role</th>
                        <th>Delete</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- Data from users.txt will be loaded here -->
                </tbody>
            </table>
        </section>
    </div>
    <div class="main-container">
        <a href="get?delete=users"><button class="button button-delete">🗑️ Delete users.txt File</button></a>
    </div>
    <script>
        // JavaScript to load and parse users.txt
        async function loadTableData() {
            try {
                const response = await fetch('view-users');
                const data = await response.text();
                const rows = data.trim().split('\n').slice(1); // Skip the header line

                const tableBody = document.querySelector('#tableData tbody');
                rows.forEach((row, index) => {
                    const columns = row.split(',');
                    const tr = document.createElement('tr');
                    // Add remaining columns
                    columns.forEach(column => {
                        const td = document.createElement('td');
                        td.textContent = column;
                        tr.appendChild(td);
                    });
                    // Create and add row number cell with a delete link
                    const noCell = document.createElement('td');
                    const deleteLink = document.createElement('a');
                    deleteLink.href = `get?delete-user=${index + 1}`;
                    deleteLink.textContent = "❌ Delete User #" + (index + 1);
                    noCell.appendChild(deleteLink);
                    tr.appendChild(noCell);

                    tableBody.appendChild(tr);
                });
            } catch (error) {
                console.error('Error loading log data:', error);
            }
        }
        // Call the function to load log data
        loadTableData();
    </script>
</body>
</html>

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

Скопируйте следующий код в файл add-user.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add User</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <div class="nav-container">
            <a href="/" class="brand">User Management</a>
            <ul class="nav-menu">
                <li><a href="/">📄 Full Log</a></li>
                <li><a href="add-user">➕ Add User</a></li>
                <li><a href="manage-users">👤 Manage Users</a></li>
            </ul>
        </div>
    </nav>
    <div class="main-container">
        <section class="main-section">
            <h2>➕ Add User</h2>
            <p>Enter the QR Code data.</p><br>
            <form action="get" class="user-form">
                <label for="qrCode">QR Code</label>
                <input type="text" id="qrCode" name="qrCode" required>
                <label for="role">Role</label>
                <select id="role" name="role">
                    <option value="admin">Admin</option>
                    <option value="user">User</option>
                </select>
                <button type="submit">✅ Save</button>
            </form>
        </section>
    </div>
</body>
</html>

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

Скопируйте следующий код в файл get.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add User</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <div class="nav-container">
            <a href="/" class="brand">User Management</a>
            <ul class="nav-menu">
                <li><a href="/">📄 Full Log</a></li>
                <li><a href="add-user">➕ Add User</a></li>
                <li><a href="manage-users">👤 Manage Users</a></li>
            </ul>
        </div>
    </nav>
    <div class="main-container">
        <section class="main-section">
            <p>%inputmessage%</p>
        </section>
    </div>
</body>
</html>

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

CSS-файл

Скопируйте следующий код в файл style.css. Вы можете изменить его по своему усмотрению.

/* General Styles */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f9;
    color: #333;
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 100vh;
    margin: 0;
}

/* Navigation Bar Styles */
nav {
    width: 100%;
    background-color: #333;
    padding: 1rem 0;
}

.nav-container {
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 1rem;
}

.brand {
    color: #fff;
    text-decoration: none;
    font-size: 1.5rem;
    font-weight: bold;
}

.nav-menu {
    list-style-type: none;
    display: flex;
}

.nav-menu li {
    margin-left: 1.5rem;
}

.nav-menu a {
    color: #fff;
    text-decoration: none;
    font-size: 1rem;
    transition: color 0.3s;
}

.nav-menu a:hover, .nav-menu a.active {
    color: #f4f4f9;
}

.main-container {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-grow: 1;
    width: 100%;
}

.main-section {
    max-width: 900px;
    padding: 2rem;
    background-color: #fff;
    border-radius: 5px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.main-section h2 {
    margin-bottom: 1rem;
    color: #333;
}

.user-form label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: bold;
    color: #333;
}

.user-form input, .user-form select {
    width: 100%;
    padding: 0.5rem;
    margin-bottom: 1rem;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.user-form button {
    width: 100%;
    padding: 0.7rem;
    background-color: #333;
    color: #fff;
    border: none;
    border-radius: 4px;
    font-size: 1rem;
    cursor: pointer;
    transition: background-color 0.3s;
}

.user-form button:hover {
    background-color: #555;
}

.button {
    display: inline-block;
    padding: 10px 20px;
    margin: 10px;
    font-size: 16px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition-duration: 0.4s;
}

.button-delete {
    background-color: #780320;
    color: #fff;
}

.button-home {
    background-color: #333;
    color: #fff;
}

#tableData {
    font-family: Arial, Helvetica, sans-serif;
    border-collapse: collapse;
    width: 100%;
  }

#tableData td, #tableData th {
    border: 1px solid #ddd;
    padding: 8px;
}

#tableData tr:nth-child(even) {
    background-color: #f2f2f2;
}

#tableData tr:hover {
    background-color: #ddd;
}

#tableData th {
    padding-top: 12px;
    padding-bottom: 12px;
    text-align: left;
    background-color: #1f1f1f;
    color: white;
}

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

Код — ESP32-CAM считыватель QR-кодов с веб-сервером

Скопируйте код считывателя QR-кодов в вашу Arduino IDE.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete instructions at https://RandomNerdTutorials.com/esp32-cam-qr-code-reader-web-server/
  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.
*********/

#include <Arduino.h>
#include <ESP32QRCodeReader.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
#include "FS.h"
#include "SD_MMC.h"
#include <time.h>
#include <WiFi.h>

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

// FOR THIS PROJECT, YOUR ESP32-CAM NEEDS TO HAVE PSRAM.
// Some of the compatible boards: CAMERA_MODEL_AI_THINKER | CAMERA_MODEL_WROVER_KIT | CAMERA_MODEL_ESP_EYE
// CAMERA_MODEL_M5STACK_PSRAM | CAMERA_MODEL_M5STACK_V2_PSRAM | CAMERA_MODEL_M5STACK_WIDE
ESP32QRCodeReader reader(CAMERA_MODEL_AI_THINKER);

long timezone = 0;
byte daysavetime = 1;

const int ledPin = 4;

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

const char* PARAM_INPUT_1 = "qrCode";
const char* PARAM_INPUT_2 = "role";
const char* PARAM_INPUT_3 = "delete";
const char* PARAM_INPUT_4 = "delete-user";

String inputMessage;
String inputParam;

void onQrCodeTask(void *pvParameters) {
  struct QRCodeData qrCodeData;

  while (true) {
    if (reader.receiveQrCode(&qrCodeData, 100)) {
      Serial.println("Scanned new QRCode");
      if (qrCodeData.valid) {
        String qrCodeString = String((const char *)qrCodeData.payload);
        Serial.print("Valid payload: ");
        Serial.println(qrCodeString);
        String role = getRoleFromFile("/users.txt", qrCodeString);
        if (role != "") {
          Serial.print("Role for QR Code: ");
          Serial.print(qrCodeString);
          Serial.print(" is ");
          Serial.println(role);
        } else {
          role = "unknown";
          Serial.print("QR Code: ");
          Serial.print(qrCodeString);
          Serial.println(" not found, set user role to unknown");
        }
        String sdMessage = qrCodeString + "," + role;
        appendFile(SD_MMC, "/log.txt", sdMessage.c_str());
        digitalWrite(ledPin, HIGH);
        delay(1000);
        digitalWrite(ledPin, LOW);
      }
      else {
        Serial.print("Invalid payload: ");
        Serial.println((const char *)qrCodeData.payload);
      }
    }
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}

// Write to the SD card
void writeFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Writing file: %s\n", path);

  File file = fs.open(path, FILE_WRITE);
  if(!file) {
    Serial.println("Failed to open file for writing");
    return;
  }
  if(file.print(message)) {
    Serial.println("File written");
  } else {
    Serial.println("Write failed");
  }
  file.close();
}

// Append data to the SD card
void appendFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Appending to file: %s\n", path);

  File file = fs.open(path, FILE_APPEND);
  if(!file) {
    Serial.println("Failed to open file for appending");
    return;
  }

  time_t t = file.getLastWrite();
  struct tm *tmstruct = localtime(&t);

  char bufferDate[50]; // Adjust buffer size as needed
  snprintf(bufferDate, sizeof(bufferDate), "%d-%02d-%02d",
          (tmstruct->tm_year) + 1900,
          (tmstruct->tm_mon) + 1,
          tmstruct->tm_mday);
  char bufferTime[50]; // Adjust buffer size as needed
  snprintf(bufferTime, sizeof(bufferTime), "%02d:%02d:%02d",
          tmstruct->tm_hour,
          tmstruct->tm_min,
          tmstruct->tm_sec);

  String lastWriteTime = bufferDate;
  String finalString = String(bufferDate) + "," + String(bufferTime) + "," + String(message) + "\n";
  Serial.println(lastWriteTime);
  if(file.print(finalString.c_str())) {
    Serial.println("Message appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();
}

// Append data to the SD card
void appendUserFile(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Appending to file: %s\n", path);

  File file = fs.open(path, FILE_APPEND);
  if(!file) {
    Serial.println("Failed to open file for appending");
    return;
  }

  String finalString = String(message) + "\n";

  if(file.print(finalString.c_str())) {
    Serial.println("Message appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();
}

void deleteFile(fs::FS &fs, const char *path) {
  Serial.printf("Deleting file: %s\n", path);
  if (fs.remove(path)) {
    Serial.println("File deleted");
  } else {
    Serial.println("Delete failed");
  }

  // If the log.txt file doesn't exist, create a file on the SD card and write the header
  File file = SD_MMC.open("/log.txt");
  if(!file) {
    Serial.println("Creating new log.txt file...");
    writeFile(SD_MMC, "/log.txt", "Date,Time,QR_Code,Role\r\n");
  }
  else {
    Serial.println("log.txt file already exists");
  }
  file.close();

  // If the users.txt file doesn't exist, create a file on the SD card and write the header
  file = SD_MMC.open("/users.txt");
  if(!file) {
    Serial.println("Creating new users.txt file...");
    writeFile(SD_MMC, "/users.txt", "QR_Code,Role\r\n");
  }
  else {
    Serial.println("users.txt file already exists");
  }
  file.close();
}

String processor(const String& var){
  return String("HTTP GET request sent to your ESP on input field ("
                + inputParam + ") with value: " + inputMessage +
                "<br><a href=\"/\"><button class=\"button button-home\">Return to Home Page</button></a>");
}

void deleteLineFromFile(const char* filename, int lineNumber) {
  File file = SD_MMC.open(filename);
  if (!file) {
    Serial.println("Failed to open file for reading.");
    return;
  }

  // Read all lines except the one to delete
  String lines = "";
  int currentLine = 0;
  while (file.available()) {
    String line = file.readStringUntil('\n');
    if (currentLine != lineNumber) {
      lines += line + "\n";
    }
    currentLine++;
  }
  file.close();

  // Write back all lines except the deleted one
  file = SD_MMC.open(filename, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing.");
    return;
  }

  file.print(lines);
  file.close();
  Serial.println("Line deleted successfully.");
}

String getRoleFromFile(const char* filename, String qrCode) {
  File file = SD_MMC.open(filename);
  if (!file) {
    Serial.println("Failed to open file for reading.");
    return "";
  }

  // Skip the header line
  file.readStringUntil('\n');

  // Read each line and check for QR Code
  while (file.available()) {
    String line = file.readStringUntil('\n');

    int commaIndex = line.indexOf(',');
    if (commaIndex > 0) {
      String fileQrCode = line.substring(0, commaIndex);
      String role = line.substring(commaIndex + 1);

      // Compare qrCode
      if (fileQrCode == qrCode) {
        file.close();
        role.trim();  // Remove any extra spaces or newline characters
        return role;
      }
    }
  }
  file.close();
  return "";  // Return empty string if qrCode not found
}

void initLittleFS() {
  if(!LittleFS.begin()){
    Serial.println("An Error has occurred while mounting LittleFS");
        return;
  }
}

void initWifi() {
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  int connectAttempt = 0;
  Serial.println("Connecting to WiFi..");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    connectAttempt ++;
    if (connectAttempt == 10){
      ESP.restart();
    }
  }
  // Print ESP32 Local IP Address
  Serial.print("\nESP IP Address: ");
  Serial.println(WiFi.localIP());
}

void initTime() {
  Serial.println("Initializing Time");
  struct tm tmstruct;
  tmstruct.tm_year = 0;
  getLocalTime(&tmstruct);
  Serial.printf(
    "Time and Date right now is : %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct.tm_year) + 1900, (tmstruct.tm_mon) + 1, tmstruct.tm_mday, tmstruct.tm_hour, tmstruct.tm_min,
    tmstruct.tm_sec
  );
}

void initSDCard() {
  if (!SD_MMC.begin("/sdcard", true)) {
    Serial.println("Card Mount Failed");
    return;
  }
  uint8_t cardType = SD_MMC.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  Serial.print("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");
  }

  uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  // If the log.txt file doesn't exist, create a file on the SD card and write the header
  File file = SD_MMC.open("/log.txt");
  if(!file) {
    Serial.println("log.txt file doesn't exist");
    Serial.println("Creating file...");
    writeFile(SD_MMC, "/log.txt", "Date,Time,QR_Code,Role\r\n");
  }
  else {
    Serial.println("log.txt file already exists");
  }
  file.close();

  // If the users.txt file doesn't exist, create a file on the SD card and write the header
  file = SD_MMC.open("/users.txt");
  if(!file) {
    Serial.println("users.txt file doesn't exist");
    Serial.println("Creating file...");
    writeFile(SD_MMC, "/users.txt", "QR_Code,Role\r\n");
  }
  else {
    Serial.println("users.txt file already exists");
  }
  file.close();
}

void setup() {
  Serial.begin(115200);  // Initialize serial communication
  while (!Serial);       // Do nothing if no serial port is opened (added for Arduinos based on ATMEGA32U4).

  reader.setup();
  Serial.println("\nSetup QRCode Reader");
  reader.beginOnCore(1);
  Serial.println("Begin on Core 1");
  xTaskCreate(onQrCodeTask, "onQrCode", 4 * 1024, NULL, 4, NULL);

  initWifi();
  initLittleFS();
  configTime(3600 * timezone, daysavetime * 3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
  initTime();
  initSDCard();

  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/full-log.html");
  });
  // Route for root /add-user web page
  server.on("/add-user", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/add-user.html");
  });
  // Route for root /manage-users web page
  server.on("/manage-users", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/manage-users.html");
  });

  // Serve Static files
  server.serveStatic("/", LittleFS, "/");

  // Loads the log.txt file
  server.on("/view-log", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SD_MMC, "/log.txt", "text/plain", false);
  });
  // Loads the users.txt file
  server.on("/view-users", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SD_MMC, "/users.txt", "text/plain", false);
  });

  // Receive HTTP GET requests on <ESP_IP>/get?input=<inputMessage>
  server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
    // GET input1 and input2 value on <ESP_IP>/get?input1=<inputMessage1>&input2=<inputMessage2>
    if (request->hasParam(PARAM_INPUT_1) && request->hasParam(PARAM_INPUT_2)) {
      inputMessage = request->getParam(PARAM_INPUT_1)->value();
      inputParam = String(PARAM_INPUT_1);
      inputMessage += " " + request->getParam(PARAM_INPUT_2)->value();
      inputParam += " " + String(PARAM_INPUT_2);

      String finalMessageInput = String(request->getParam(PARAM_INPUT_1)->value()) + "," + String(request->getParam(PARAM_INPUT_2)->value());
      appendUserFile(SD_MMC, "/users.txt", finalMessageInput.c_str());
    }
    else if (request->hasParam(PARAM_INPUT_3)) {
      inputMessage = request->getParam(PARAM_INPUT_3)->value();
      inputParam = String(PARAM_INPUT_3);
      if(request->getParam(PARAM_INPUT_3)->value()=="users") {
        deleteFile(SD_MMC, "/users.txt");
      }
      else if(request->getParam(PARAM_INPUT_3)->value()=="log") {
        deleteFile(SD_MMC, "/log.txt");
      }
    }
    else if (request->hasParam(PARAM_INPUT_4)) {
      inputMessage = request->getParam(PARAM_INPUT_4)->value();
      inputParam = String(PARAM_INPUT_4);
      deleteLineFromFile("/users.txt", inputMessage.toInt());
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    request->send(LittleFS, "/get.html", "text/html", false, processor);
  });
  // Start server
  server.begin();
}

void loop() {

}

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

Перед загрузкой кода необходимо вставить ваши учётные данные сети в следующие строки, чтобы ESP32-CAM могла установить Wi-Fi соединение.

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

Для этого проекта мы используем модель AI-Thinker ESP32-CAM, однако, если вы используете другую плату, вам нужно указать её здесь:

// FOR THIS PROJECT, YOUR ESP32-CAM NEEDS TO HAVE PSRAM.
// Some of the compatible boards: CAMERA_MODEL_AI_THINKER | CAMERA_MODEL_WROVER_KIT | CAMERA_MODEL_ESP_EYE
// CAMERA_MODEL_M5STACK_PSRAM | CAMERA_MODEL_M5STACK_V2_PSRAM | CAMERA_MODEL_M5STACK_WIDE

ESP32QRCodeReader reader(CAMERA_MODEL_AI_THINKER);

Примечание: ваша плата должна иметь PSRAM.

Ресурсы для понимания кода

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

Вот список уроков, которые стоит изучить для понимания затронутых тем:

1) Считывание QR-кодов: ESP32-CAM QR Code Reader/Scanner (Arduino IDE)

2) Получение временной метки с NTP-сервера: ESP32 NTP Client-Server: Get Date and Time (Arduino IDE)

3) Чтение и запись на/с карты microSD: ESP32: Guide for MicroSD Card Module using Arduino IDE

4) Создание веб-сервера с ESP32: список всех наших проектов веб-серверов

5) Отправка данных с веб-страницы на ESP32 через HTML-форму: Input Data on HTML Form ESP32/ESP8266 Web Server using Arduino IDE

6) Узнайте больше о ESP32-CAM из нашей электронной книги Build ESP32-CAM Projects using Arduino IDE.

Загрузка кода на ESP32-CAM AI-Thinker с помощью USB-программатора ESP32-CAM-MB

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

ESP32-CAM-MB Micro USB Programmer CH340G Serial Chip OV2640 Camera

Если у вас есть FTDI-программатор, вы можете следовать этому руководству: How to Program / Upload Code to ESP32-CAM AI-Thinker (Arduino IDE using an FTDI programmer)

Затем подключите плату к компьютеру с помощью USB-кабеля.

После этого в Arduino IDE перейдите в Tools > Board и выберите AI-Thinker ESP32-CAM. Или найдите эту плату в верхней строке поиска. У вас должно быть установлено дополнение ESP32. В противном случае эта плата не появится в меню Boards.

Выбор AI-Thinker ESP32-CAM в Arduino IDE

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

Примечание: если плата не отображается, значит, вероятно, у вас не установлены драйверы CH340C. Найдите в Google «CH340C drivers» для вашей операционной системы и установите драйверы.

Наконец, нажмите кнопку Upload в Arduino IDE.

Загрузка программы ESP32-CAM в Arduino IDE

Готово! Ваш код сканера QR-кодов должен быть запущен на ESP32-CAM.

Загрузка кода и папки data

После ввода учётных данных сети сохраните код. Перейдите в Sketch > Show Sketch Folder и создайте папку с названием data.

Arduino IDE — открытие папки скетча для создания папки data

Внутри этой папки вы должны разместить HTML- и CSS-файлы, представленные ранее.

Загрузка образа файловой системы

Загрузите эти файлы в файловую систему: нажмите [Ctrl] + [Shift] + [P] на Windows или [Cmd] + [Shift] + [P] на MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.

Если этот вариант недоступен, значит вы не установили плагин загрузки файловой системы. Ознакомьтесь с этим руководством.

Загрузка файлов LittleFS в ESP32 через Arduino IDE

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

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

После загрузки кода и файлов из папки data откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку EN/RST на ESP32-CAM. Плата должна инициализировать сканер QR-кодов и веб-сервер. Проверьте окно Serial Monitor в Arduino IDE, чтобы убедиться, что всё работает правильно.

ESP32-CAM QR Code Reader Web Server — демонстрация Serial Monitor

Должен быть напечатан IP-адрес ESP32. В моём случае это 192.168.1.76. Сохраните свой IP-адрес, он понадобится на следующем шаге.

Тестирование считывателя QR-кодов

Вот 4 примера QR-кодов (соответствующие данные указаны в подписи). Вы можете использовать любой онлайн-генератор QR-кодов для создания пользовательских QR-кодов с нужными данными.

QR-код rui_santos

rui_santos

QR-код sara_santos

sara_santos

QR-код test_user_1

test_user_1

QR-код test_user_2

test_user_2

Направьте ESP32-CAM на QR-код и держите камеру стабильно.

ESP32-CAM Camera QR Code Scanner Reader Testing

Данные QR-кода должны быть напечатаны в Serial Monitor Arduino IDE:

ESP32-CAM QR Code Reader — демонстрация сканирования QR-кода

Если в Serial Monitor Arduino IDE вы постоянно получаете сообщение:

Invalid payload: ECC failure

Возможно, вам нужно использовать QR-код меньшего размера, сделать камеру более стабильной и направить её непосредственно на QR-код при лучших условиях освещения.

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

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

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

ESP32-CAM QR Code Reader Web Server — таблица полного журнала

Сканируйте несколько QR-кодов с помощью ESP32-CAM. Каждый раз при сканировании действительного QR-кода должен загораться встроенный светодиод-вспышка. В Serial Monitor Arduino IDE будут напечатаны данные QR-кода:

ESP32-CAM QR Code Reader Web Server — сканирование QR-кода

Для тестирования рекомендуется сканировать несколько QR-кодов, чтобы на веб-сервере отображалось больше данных. Скопируйте данные одного из ваших QR-кодов (например: rui_santos). Затем откройте вкладку Add User.

ESP32 — открытие вкладки Add User

Введите данные QR-кода и выберите роль (user или admin). Наконец, нажмите кнопку «Save». Я повторил этот процесс для остальных QR-кодов, упомянутых в этой статье.

ESP32-CAM QR Code Reader Web Server — добавление нового пользователя с ролью

Теперь, если вы перейдёте на веб-страницу Manage Users:

ESP32 — открытие вкладки Manage Users

Загружается таблица со всеми QR-кодами и соответствующими ролями пользователей. Вы можете нажать «X», чтобы удалить пользователя.

ESP32-CAM QR Code Reader Web Server — удаление пользователя

Сканируйте QR-коды ещё несколько раз, затем откройте главную страницу веб-сервера. Таблица журнала должна содержать все записи с временной меткой, QR-кодом и соответствующими ролями пользователей.

ESP32-CAM QR Code Reader Web Server — полная таблица журнала

Внизу страниц Full Log и Manage Users есть возможность удалить файлы log.txt и users.txt с карты microSD в любое время.

ESP32 — удаление файлов log.txt или users.txt

Вы также можете получить доступ к веб-серверу на своём смартфоне.

ESP32-CAM QR Code Reader Web Server — демонстрация проекта управления пользователями

Устранение неполадок и советы

Как мы упоминали ранее, этот сканер QR-кодов использует много памяти, поэтому ваша ESP32-CAM должна иметь PSRAM. Вот некоторые платы, которые были протестированы и должны работать с этим примером:

  • CAMERA_MODEL_AI_THINKER

  • CAMERA_MODEL_WROVER_KIT

  • CAMERA_MODEL_ESP_EYE

  • CAMERA_MODEL_M5STACK_PSRAM

  • CAMERA_MODEL_M5STACK_V2_PSRAM

  • CAMERA_MODEL_M5STACK_WIDE

Если вы получаете какую-либо из следующих ошибок, прочитайте наше руководство ESP32-CAM Troubleshooting Guide: Most Common Problems Fixed:

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

  • Camera init failed with error 0x20001 or similar

  • Brownout detector or Guru meditation error

  • Sketch too big error – Wrong partition scheme selected

  • Board at COMX is not available – COM Port Not Selected

  • Psram error: GPIO isr service is not installed

Заключение

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

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