ESP32-CAM: считыватель QR-кодов с веб-сервером управления пользователями
В этом проекте вы создадите систему управления пользователями по QR-кодам с веб-сервером, работающим на ESP32-CAM. ESP32-CAM постоянно использует камеру для сканирования новых QR-кодов с помощью библиотеки ESP32QRCodeReader и модифицированной версии библиотеки quirc. Когда плата обнаруживает действительный QR-код, она сохраняет данные QR-кода на карту MicroSD. Вы можете получить доступ к веб-серверу в любом браузере для управления пользователями и просмотра полного журнала. ESP32-CAM будет программироваться с помощью Arduino IDE.
Обзор проекта
На следующей схеме показан общий обзор работы проекта.
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 — читайте Лучшие платы ESP32-CAM (или другая ESP32-CAM с камерой OV2640)
Рекомендуется — ESP32-CAM-MB Micro USB Programmer или FTDI программатор
Если вы используете плату 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 и установите только что скачанную библиотеку.
Установка библиотек Async Web Server
Мы создадим веб-сервер с использованием следующих библиотек:
ESPAsyncWebServer от ESP32Async
AsyncTCP от ESP32Async
Вы можете установить эти библиотеки через менеджер библиотек Arduino. Откройте менеджер библиотек, нажав на значок библиотеки в левой боковой панели.
Найдите ESPAsyncWebServer и установите ESPAsyncWebServer от ESP32Async.
Затем установите библиотеку AsyncTCP. Найдите AsyncTCP и установите AsyncTCP от ESP32Async.
Плагин загрузчика LittleFS
Файлы, используемые для создания веб-сервера, будут сохранены в файловой системе LittleFS на ESP32. Убедитесь, что у вас установлен плагин LittleFS Uploader в Arduino IDE. Вы можете следовать этому руководству:
Подготовка карты MicroSD
Перед продолжением убедитесь, что вы отформатировали карту microSD в формате FAT32. Следуйте приведённым ниже инструкциям для форматирования карты microSD или используйте программу SD Card Formatter (совместима с Windows и Mac OS).
1. Вставьте карту microSD в компьютер. Перейдите в Мой компьютер и щёлкните правой кнопкой мыши по SD-карте. Выберите Форматирование, как показано на рисунке ниже.
2. Появится новое окно. Выберите FAT32, нажмите Начать, чтобы инициализировать процесс форматирования, и следуйте инструкциям на экране.
Подключите карту microSD к ESP32-CAM:
Чтобы узнать, как использовать карту microSD с ESP32-CAM, вы можете прочитать следующий урок:
Организация файлов
Для удобства организации проекта мы создадим 6 файлов для веб-сервера:
Скетч Arduino: для обработки веб-сервера, считывателя QR-кодов и карты microSD;
full-log.html: загружает журнал всех успешно сканированных QR-кодов и данных пользователей;
manage-users.html: веб-страница, позволяющая просматривать и удалять пользователей;
add-user.html: веб-страница, позволяющая добавлять новых пользователей с уникальным QR-кодом;
get.html: обрабатывает все HTTP GET-запросы;
style.css: для стилизации веб-страницы.
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 программатор к вашей плате (вы можете узнать, как он работает, прочитав это руководство).
Если у вас есть 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.
Перейдите в Tools > Port и выберите COM-порт, к которому подключена ESP32-CAM.
Примечание: если плата не отображается, значит, вероятно, у вас не установлены драйверы CH340C. Найдите в Google «CH340C drivers» для вашей операционной системы и установите драйверы.
Наконец, нажмите кнопку Upload в Arduino IDE.
Готово! Ваш код сканера QR-кодов должен быть запущен на ESP32-CAM.
Загрузка кода и папки data
После ввода учётных данных сети сохраните код. Перейдите в Sketch > Show Sketch Folder и создайте папку с названием data.
Внутри этой папки вы должны разместить HTML- и CSS-файлы, представленные ранее.
Загрузка образа файловой системы
Загрузите эти файлы в файловую систему: нажмите [Ctrl] + [Shift] + [P] на Windows или [Cmd] + [Shift] + [P] на MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.
Если этот вариант недоступен, значит вы не установили плагин загрузки файловой системы. Ознакомьтесь с этим руководством.
Важно: убедитесь, что Serial Monitor закрыт перед загрузкой в файловую систему. В противном случае загрузка не удастся.
Загрузка кода
После загрузки кода и файлов из папки data откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку EN/RST на ESP32-CAM. Плата должна инициализировать сканер QR-кодов и веб-сервер. Проверьте окно Serial Monitor в Arduino IDE, чтобы убедиться, что всё работает правильно.
Должен быть напечатан IP-адрес ESP32. В моём случае это 192.168.1.76. Сохраните свой IP-адрес, он понадобится на следующем шаге.
Тестирование считывателя QR-кодов
Вот 4 примера QR-кодов (соответствующие данные указаны в подписи). Вы можете использовать любой онлайн-генератор QR-кодов для создания пользовательских QR-кодов с нужными данными.
rui_santos
sara_santos
test_user_1
test_user_2
Направьте ESP32-CAM на QR-код и держите камеру стабильно.
Данные QR-кода должны быть напечатаны в Serial Monitor Arduino IDE:
Если в Serial Monitor Arduino IDE вы постоянно получаете сообщение:
Invalid payload: ECC failure
Возможно, вам нужно использовать QR-код меньшего размера, сделать камеру более стабильной и направить её непосредственно на QR-код при лучших условиях освещения.
ESP32-CAM имеет ограничения по разрешению и плохо работает с движением, поэтому вам нужно стабильно направлять камеру на QR-код. Кроме того, распознавание QR-кодов может быть затруднено при плохих условиях освещения, будь то слишком яркий или слишком тёмный свет.
Демонстрация
Откройте браузер в вашей локальной сети и введите IP-адрес ESP32-CAM. Вы должны получить доступ к веб-серверу, который выглядит следующим образом — таблица по умолчанию должна быть пустой.
Сканируйте несколько QR-кодов с помощью ESP32-CAM. Каждый раз при сканировании действительного QR-кода должен загораться встроенный светодиод-вспышка. В Serial Monitor Arduino IDE будут напечатаны данные QR-кода:
Для тестирования рекомендуется сканировать несколько QR-кодов, чтобы на веб-сервере отображалось больше данных. Скопируйте данные одного из ваших QR-кодов (например: rui_santos). Затем откройте вкладку Add User.
Введите данные QR-кода и выберите роль (user или admin). Наконец, нажмите кнопку «Save». Я повторил этот процесс для остальных QR-кодов, упомянутых в этой статье.
Теперь, если вы перейдёте на веб-страницу Manage Users:
Загружается таблица со всеми QR-кодами и соответствующими ролями пользователей. Вы можете нажать «X», чтобы удалить пользователя.
Сканируйте QR-коды ещё несколько раз, затем откройте главную страницу веб-сервера. Таблица журнала должна содержать все записи с временной меткой, QR-кодом и соответствующими ролями пользователей.
Внизу страниц Full Log и Manage Users есть возможность удалить файлы log.txt и users.txt с карты microSD в любое время.
Вы также можете получить доступ к веб-серверу на своём смартфоне.
Устранение неполадок и советы
Как мы упоминали ранее, этот сканер 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, вы можете прочитать следующие руководства: