In this project, you’ll build a QR code user management system with a web server that runs on an ESP32-CAM. The ESP32-CAM is constantly using its camera to scan for new QR codes using the ESP32QRCodeReader library and a modified version of the quirc library. When it detects a valid QR code, it stores the QR code data on the MicroSD card. You can access the web server in any browser to manage the users and view the complete log. The ESP32-CAM will be programmed using Arduino IDE.

Project Overview
The following diagram shows a high-level overview of how the project works.

1) When the ESP32-CAM detects a valid QR code, it reads its data and the built-in LED flash will blink.
2) The ESP32-CAM records the time of that interaction and saves the time and QR code data on the microSD card on a file called log.txt.
3) The ESP32-CAM also hosts a web server to display and manage the information from the microSD card.
4) The root (/) URL shows the complete log (saved on the microSD card module log.txt) with the timestamp and the QR code data.
5) There’s another page on the /add-user path that allows you to add users and their roles using a form.
6) The data entered via this form will be saved on the user.txt file saved on the microSD card.
7) There is another page on the /manage-users path that allows you to consult and delete users.
8) This page allows you to interact with the users.txt file.
Parts Required
We’ll be using the ESP32-CAM board labeled as AI-Thinker module, but other modules should also work by making the correct pin assignment in the code. The ESP32-CAM board is a $9 device (or less) that combines an ESP32-S chip, an OV2640 camera, a microSD card slot, and several GPIO pins.

To follow this tutorial you need the following components:
- ESP32-CAM with OV2640 – read Best ESP32-CAM Dev Boards (or another ESP32-CAM with the OV2640 camera)
- Recommended – ESP32-CAM-MB Micro USB Programmer or FTDI programmer
- MicroSD Card
If you’re using an ESP32 camera board without microSD card support, you also need a microSD card module:
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
For an introduction to the ESP32-CAM, you can follow the next tutorials:
- ESP32-CAM Video Streaming with Arduino IDE
- ESP32-CAM AI-Thinker Pinout Guide: GPIOs Usage Explained
- ESP32-CAM Troubleshooting Guide
- ESP32-CAM QR Code Reader/Scanner (Arduino IDE)
- Build ESP32-CAM Projects using Arduino IDE (eBook)
Preparing the Arduino IDE
We’ll program the ESP32 board using Arduino IDE. So you need the Arduino IDE installed as well as the ESP32 add-on. You can follow the next tutorial to install the ESP32 add-on, if you haven’t already:
Installing the ESP32QRCodeReader Library
For this tutorial, we’ll use the ESP32QRCodeReader library by Alvarowolfx, which makes it easy to read QR codes with the ESP32-CAM board.
Then, in the Arduino IDE, go to Sketch > Include Library > Add .ZIP library and install the library that you’ve just downloaded in the previous link.

Installing the Async Web Server Libraries
We’ll build the web server using the following libraries:
- ESPAsyncWebServer by ESP32Async
- AsyncTCP by ESP32Async
You can install these libraries in the Arduino Library Manager. Open the Library Manager by clicking the Library icon at the left sidebar.
Search for ESPAsyncWebServer and install the ESPAsyncWebServer by ESP32Async.

Then, install the AsyncTCP library. Search for AsyncTCP and install the AsyncTCP by ESP32Async.

LittleFS Uploader Plugin
The files used to build the web server will be saved on the ESP32 LittleFS filesystem. Make sure you have the LittleFS Uploader Plugin installed in your Arduino IDE. You can follow this tutorial:
Preparing the MicroSD Card
Before proceeding with the tutorial, make sure you format your microSD card as FAT32. Follow the next instructions to format your microSD card or use a software tool like SD Card Formatter (compatible with Windows and Mac OS).
1. Insert the microSD card into your computer. Go to My Computer and right-click on the SD card. Select Format as shown in the figure below.

2. A new window pops up. Select FAT32, press Start to initialize the formatting process, and follow the onscreen instructions.

Attach the microSD card to the ESP32-CAM:

To learn how to use the microSD card with the ESP32-CAM, you can read the next tutorial:
Organizing Your Files
To keep the project organized and make it easier to understand, we’ll create 6 files to build the web server:
- Arduino sketch: to handle the web server, QR Code reader, and microSD card;
- full-log.html: loads the log of every QR Code that has been successfully scanned and its user data;
- manage-users.html: web page that allows you to view and delete users;
- add-user.html: web page that allows you to add new users with a unique QR Code;
- get.html: handles all the HTTP GET requests;
- style.css: to style the web page.

You should save the HTML and CSS files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We’ll upload these files to the ESP32-CAM filesystem (LittleFS).
You can download all project files:
HTML Files
Copy the following to the full-log.html file.
<!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>
Copy the following to the manage-users.html file.
<!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>
Copy the following to the add-user.html file.
<!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>
Copy the following to the get.html file.
<!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 File
Copy the following to the style.css file. Feel free to change it to make the web page look as you wish.
/* 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;
}
Code – ESP32-CAM QR Code Reader Web Server
Copy the QR Code reader code to your 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() {
}
Before uploading the code, you need to insert your network credentials in the following lines so that the ESP32-CAM can establish a Wi-Fi connection.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_SSID";
For this project, we are using the AI-Thinker ESP32-CAM model, however, if you are using another board you need to define it here:
// 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);
Note: your board needs to have PSRAM.
Resources to Help You Understand the Code
This code is quite long and we won’t explain in detail how it works because all subjects covered here were already explained in previous projects.
Here’s a list of the tutorials you should look at to learn about the subjects covered here:
1) Reading QR Codes: ESP32-CAM QR Code Reader/Scanner (Arduino IDE)
2) Getting a timestamp using an NTP server: ESP32 NTP Client-Server: Get Date and Time (Arduino IDE).
3) Reading and writing from/to the microSD card: ESP32: Guide for MicroSD Card Module using Arduino IDE.
4) Creating a web server with the ESP32: a list of all our web server projects.
5) Sending data from a web page to the ESP32 via HTML form: Input Data on HTML Form ESP32/ESP8266 Web Server using Arduino IDE.
6) Learn more about the ESP32-CAM with our dedicated eBook Build ESP32-CAM Projects using Arduino IDE.
Upload Code to the ESP32-CAM AI-Thinker using the ESP32-CAM-MB USB Programmer
To upload code to the ESP32-CAM board, we’ll use the ESP32-CAM-MB programmer. Attach the ESP32-CAM-MB micro USB programmer to your board (you can learn how it works by reading this guide).

If you have an FTDI programmer, you can follow this tutorial instead: How to Program / Upload Code to ESP32-CAM AI-Thinker (Arduino IDE using an FTDI programmer).
Then, connect the board to your computer using a USB cable.
After that, in your Arduino IDE, go to Tools > Board and select the AI-Thinker ESP32-CAM. Or search for that board on the top bar. You must have the ESP32 add-on installed. Otherwise, this board won’t show up on the Boards menu.

Go to Tools > Port and select the COM port the ESP32-CAM is connected to.
Note: if the board doesn’t show up, it means that you probably don’t have the CH340C drivers installed on your computer. Go to Google and search “CH340C drivers” followed by your operating system and install the drivers.
Finally, click the Upload button in your Arduino IDE.

And that’s it! Your QR code scanner code should be running in your ESP32-CAM.
Upload Code and Data Folder
After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.

Inside that folder, you should place the HTML, and CSS files provided previously.
Uploading the Filesystem Image
Upload those files to the filesystem: press [Ctrl] + [Shift] + [P] on Windows or [⌘] + [Shift] + [P] on MacOS to open the command palette. Search for the Upload LittleFS to Pico/ESP8266/ESP32 command and click on it.
If you don’t have this option is because you didn’t install the filesystem uploader plugin. Check this tutorial.

Important: make sure the Serial Monitor is closed before uploading to the filesystem. Otherwise, the upload will fail.
Uploading the Code
After uploading the code and the files in the data folder, open the Serial Monitor at a baud rate of 115200. Press the ESP32-CAM EN/RST button. It should initialize the QR code scanner and the web server. Check the Arduino IDE Serial Monitor window to see if everything is working as expected.

It should print the ESP32 IP address. In my case it’s 192.168.1.76. Save your IP because you’ll need it in the next step.
Testing the QR Code Reader
Here are 4 sample QR codes(their corresponding data is in the image caption). You can use any online QR code generator to create custom QR codes with your desired data.
Point the ESP32-CAM to a QR code and hold it steady.

The payload data should be printed in the Arduino IDE Serial Monitor:

If in the Arduino IDE Serial Monitor, you keep getting this message:
Invalid payload: ECC failure
You might need to use a smaller QR code, make the camera more stable, and point it directly to the QR code with better lighting conditions.
The ESP32-CAM has some limitations in resolution and struggles with motion, so you need to aim it steadily at the QR code. Also, detecting QR codes can be difficult in poor lighting conditions, whether it’s too bright or too dark.
Demonstration
Open a browser on your local network and type the ESP32-CAM IP address. You should get access to the web server page that looks like this, it should have a blank table by default.

Scan some QR codes with your ESP32-CAM. Every time you scan a valid QR code, the built-in flash LED should light up. In your Arduino IDE Serial Monitor, the QR data will be printed:

For testing purposes, I recommend scanning multiple QR codes, so that you have more data displayed in your web server. Copy the data of one of your QR codes (for example: rui_santos). Then, open the Add User tab.

Type the QR code data and select the role (user or admin). Finally, click the “Save” button. I repeated this process for the other QR codes mentioned in this post.

Now, if you browse to the Manager Users web page:

It loads a table with all the QR Codes and their corresponding user roles, you can click the “X” to delete a user.

Scan the QR codes a few more times, then open the web server home page. The log table should have all the entries with a timestamp, QR code and the corresponding user roles.

At the bottom of the Full Log and Manager Users web pages, you have the option to delete the log.txt and users.txt files from the microSD card at any time.

You can also access the web server page on your smartphone.

Troubleshooting and Tips
As we’ve mentioned earlier, this QR Code scanner uses a lot of memory, so your ESP32-CAM must have PSRAM. Here are some boards that were tested and should work with this example:
- 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
If you’re getting any of the following errors, read our 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
Wrapping Up
In this tutorial, you combined different subjects to build a QR Code Management System and Logger with the ESP32-CAM. Here’s a list of the subjects we covered: different web server features, getting a timestamp, datalogging, interfacing the built-in camera as a QR code reader, reading and writing files to a microSD card, and much more.
We hope you found this project useful, if you want to learn more about the ESP32-CAM you can read the following guides:
- ESP32-CAM QR Code Reader/Scanner (Arduino IDE)
- ESP32-CAM PIR Motion Detector with Photo Capture (saves to microSD card)
- ESP32-CAM AI-Thinker Pinout Guide: GPIOs Usage Explained
- Build ESP32-CAM Projects (eBook)
- Read all our ESP32-CAM Projects, Tutorials and Guides
Thank you for reading.