final summary
I was able to design, construct and test a fingerprint attendance system based on ESP32 microcontroller. All the requirements that I had set at the commencement of the project are met by the system.
The main results of this project are:
An all-fingerprint enrollment system, which records two scans of each fingerprint, makes sure that the scans are identical and stores the template in the sensor memory. The system recognizes all the fingerprints that have been stored in the flash memory of ESP32 using the student ID and name.
A system where there is an attendance marking system which can only be initiated by the professors. The learners can mark attendance only when the session is in progress and the system will not permit a student to mark attendance on the same day or session.
LCD front end to guide the user through all the functioning with clear instructions and feedback. The enrollments, marking attendance and deletions can be made without the user viewing a computer screen.
A connection to a Google Sheets, which will automatically upload the attendance records. The information is presented in spreadsheet which may be viewed using any internet-enabled device.
An HTTP implementation that is not blocking, and which makes the attendance timer remain responsive to network operations.
Huge error management which allows the user to recover in case of an error.
The total cost of the components was approximately fifty pounds, which demonstrates that it is possible to create the biometric attendance systems at a low cost.
Final Code:
Code:
/***************************************************
Complete Fingerprint Attendance System for ESP32
with Google Sheets Integration
Features:
- String Student IDs (e.g., M00814431)
- Auto-mapping to internal numeric IDs
- Enroll with type first (Student/Professor)
- Delete requires fingerprint confirmation
- Professor authentication to start/stop attendance
- Professor authentication to set RTC time
- Attendance timer with countdown display
- Clear attendance records (requires professor fingerprint)
- Real-time clock with NTP sync and manual set
- LCD display with timestamp
- Data stored in SPIFFS
- Automatic upload to Google Sheets (No IP address shown)
- UK/England Timezone
- Non-blocking HTTP requests (timer continues during upload)
****************************************************/
#include <Adafruit_Fingerprint.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <SPIFFS.h>
#include <WiFi.h>
#include <time.h>
#include <HTTPClient.h>
// I2C LCD Setup (Try 0x27 or 0x3F)
LiquidCrystal_I2C lcd(0x27, 16, 2);
// RTC Setup
RTC_DS3231 rtc;
// YOUR WiFi Credentials
const char* ssid = "WPAI";
const char* password = "irhaarham05";
// Google Sheets Web App URL (Working URL)
const char* googleScriptUrl = "https://script.google.com/macros/s/AKfycbzpHak-xQz_CXx-6CmFllDBksgfUzpEs-I74fcij_cRk7CxVJvIixiRKqHcBc93wBeJ/exec";
// UK/England Timezone (GMT+0, with DST)
const long gmtOffset_sec = 0; // UK is GMT+0
const int daylightOffset_sec = 3600; // 1 hour for British Summer Time (BST)
// Attendance Timer Settings (in minutes)
const int attendanceDuration = 10; // 10 minutes attendance window
// Fingerprint Sensor Setup for ESP32
HardwareSerial mySerial(2);
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&mySerial);
// System Variables
bool attendanceActive = false;
bool professorAuthenticated = false;
int currentMode = 0;
unsigned long lastActivity = 0;
const unsigned long inactivityTimeout = 120000; // 2 minutes for attendance mode
unsigned long attendanceStartTime = 0;
unsigned long attendanceEndTime = 0;
int remainingSeconds = 0;
String currentSessionId = "";
// Non-blocking HTTP request variables
bool isUploading = false;
unsigned long uploadStartTime = 0;
String pendingStudentId = "";
String pendingName = "";
String pendingDate = "";
String pendingTime = "";
// Data structures
struct UserData {
String studentId;
String name;
String type;
uint8_t fpId;
bool active;
};
UserData users[128];
int userCount = 0;
uint8_t nextAvailableFPId = 1;
// Attendance log for current session
struct AttendanceRecord {
String studentId;
String name;
bool marked;
};
AttendanceRecord currentSession[128];
int sessionCount = 0;
// Permanent attendance log
struct AttendanceLog {
String studentId;
String name;
String timestamp;
String date;
};
AttendanceLog logs[500];
int logCount = 0;
// Current enrollment variables
String currentEnrollType = "";
String currentEnrollStudentId = "";
String currentEnrollName = "";
uint8_t currentEnrollFPId = 0;
int enrollStep = 0;
// Time set variables
bool timeSetModeActive = false;
bool timeSetAuthenticated = false;
String pendingTimeString = "";
bool manualTimeSet = false;
// Delete mode variables
bool deleteModeActive = false;
bool deleteConfirmed = false;
uint8_t deleteFPId = 0;
String deleteStudentId = "";
String deleteName = "";
unsigned long deleteStartTime = 0;
// Clear records mode variables
bool clearModeActive = false;
bool clearConfirmed = false;
unsigned long clearStartTime = 0;
// Professor name for session logging
String professorName = "";
// URL Encoding Function
String urlEncode(String str) {
String encoded = "";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == ' ') {
encoded += "%20";
} else if (c == '|') {
encoded += "%7C";
} else if (c == ':') {
encoded += "%3A";
} else if (c == '-') {
encoded += "%2D";
} else if (c == '_') {
encoded += "%5F";
} else if (c == '/') {
encoded += "%2F";
} else if (c == '&') {
encoded += "%26";
} else if (c == '#') {
encoded += "%23";
} else {
encoded += c;
}
}
return encoded;
}
// WiFi connection flag
bool wifiConnected = false;
void setup() {
Serial.begin(115200);
delay(100);
// Initialize SPIFFS for data storage
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount Failed");
return;
}
// Initialize LCD
Wire.begin(21, 22);
lcd.init();
lcd.backlight();
// Initialize RTC
if (!rtc.begin()) {
Serial.println("Couldn't find RTC");
lcd.setCursor(0, 0);
lcd.print("RTC Error!");
while (1);
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Initializing...");
lcd.setCursor(0, 1);
lcd.print("RTC & WiFi");
// Initialize RTC time
initializeRTC();
// Initialize Fingerprint Sensor
mySerial.begin(57600, SERIAL_8N1, 16, 17);
finger.begin(57600);
delay(100);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Initializing...");
lcd.setCursor(0, 1);
lcd.print("Fingerprint...");
if (finger.verifyPassword()) {
Serial.println("Fingerprint sensor found!");
lcd.setCursor(0, 1);
lcd.print("Sensor OK! ");
delay(1000);
} else {
Serial.println("Fingerprint sensor not found!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Sensor Error!");
lcd.setCursor(0, 1);
lcd.print("Check Wiring!");
while (1);
}
// Load user data from SPIFFS
loadUserData();
loadAttendanceLogs();
findNextAvailableFPId();
getProfessorName();
// Get sensor info
finger.getParameters();
finger.getTemplateCount();
Serial.println("\n=========================================");
Serial.println(" SYSTEM INITIALIZATION COMPLETE");
Serial.println("=========================================");
Serial.print("Total fingerprints stored in sensor: ");
Serial.println(finger.templateCount);
Serial.print("Total users in database: ");
Serial.println(userCount);
Serial.print("Next available internal ID: ");
Serial.println(nextAvailableFPId);
Serial.print("Total attendance records: ");
Serial.println(logCount);
// Display current RTC time
displayCurrentTime();
// Verify WiFi connection before proceeding
verifyWiFiConnection();
showMainMenu();
}
void verifyWiFiConnection() {
Serial.println("\n=========================================");
Serial.println(" CHECKING WiFi CONNECTION");
Serial.println("=========================================");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Checking WiFi...");
// Check if already connected
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi already connected!");
wifiConnected = true;
lcd.setCursor(0, 1);
lcd.print("WiFi Connected!");
delay(2000);
return;
}
// Try to connect to WiFi
Serial.print("Connecting to WiFi");
lcd.setCursor(0, 1);
lcd.print("Connecting...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
lcd.setCursor(0, 1);
lcd.print("Connecting");
for(int i = 0; i < (attempts % 4); i++) lcd.print(".");
delay(500);
}
Serial.println("");
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi Connected Successfully!");
wifiConnected = true;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WiFi Connected!");
lcd.setCursor(0, 1);
lcd.print("Ready!");
delay(3000);
} else {
Serial.println("\nWiFi Connection Failed!");
Serial.println("Will continue in offline mode");
Serial.println("Google Sheets upload will not work");
wifiConnected = false;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WiFi Failed!");
lcd.setCursor(0, 1);
lcd.print("Offline Mode");
delay(3000);
}
Serial.println("=========================================\n");
}
void getProfessorName() {
for (int i = 0; i < userCount; i++) {
if (users[i].type == "PROFESSOR" && users[i].active) {
professorName = users[i].name;
break;
}
}
}
void initializeRTC() {
DateTime now = rtc.now();
if (now.year() < 2024 || now.year() > 2030) {
Serial.println("RTC has invalid time! Setting from NTP...");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Syncing Time...");
if (syncWithNTP()) {
Serial.println("Time synced successfully from NTP!");
} else {
Serial.println("Failed to sync with NTP. Please set time manually.");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Set Time via");
lcd.setCursor(0, 1);
lcd.print("Professor FP");
delay(3000);
}
} else {
Serial.println("RTC has valid time");
displayCurrentTime();
}
}
bool syncWithNTP() {
// Connect to WiFi for NTP sync
if (WiFi.status() != WL_CONNECTED) {
Serial.print("Connecting to WiFi for NTP sync");
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("\nWiFi connection failed for NTP!");
return false;
}
}
Serial.println("\nWiFi connected for NTP!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WiFi Connected");
lcd.setCursor(0, 1);
lcd.print("Getting Time...");
delay(1000);
configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org", "time.nist.gov");
struct tm timeinfo;
int retry = 0;
while (!getLocalTime(&timeinfo) && retry < 10) {
Serial.print(".");
delay(1000);
retry++;
}
if (retry < 10) {
rtc.adjust(DateTime(timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec));
Serial.println("\nTime synced from NTP!");
displayCurrentTime();
return true;
} else {
Serial.println("\nFailed to get time from NTP!");
return false;
}
}
void setManualTime(String timeString) {
if (timeString.length() < 19) {
Serial.println("Invalid format! Use: YYYY-MM-DD HH:MM:SS");
Serial.print("Enter time: ");
return;
}
int year = timeString.substring(0, 4).toInt();
int month = timeString.substring(5, 7).toInt();
int day = timeString.substring(8, 10).toInt();
int hour = timeString.substring(11, 13).toInt();
int minute = timeString.substring(14, 16).toInt();
int second = timeString.substring(17, 19).toInt();
if (year >= 2024 && year <= 2030 && month >= 1 && month <= 12 && day >= 1 && day <= 31 &&
hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 && second >= 0 && second <= 59) {
rtc.adjust(DateTime(year, month, day, hour, minute, second));
Serial.println("Time set manually!");
displayCurrentTime();
manualTimeSet = false;
showMainMenu();
} else {
Serial.println("Invalid time values! Please try again.");
Serial.println("Format: YYYY-MM-DD HH:MM:SS");
Serial.print("Enter: ");
}
}
void displayCurrentTime() {
DateTime now = rtc.now();
Serial.println("\n+---------------------------------------+");
Serial.print("| Current RTC Time: ");
Serial.print(now.year());
Serial.print("/");
Serial.print(now.month());
Serial.print("/");
Serial.print(now.day());
Serial.print(" ");
Serial.print(now.hour());
Serial.print(":");
Serial.print(now.minute());
Serial.print(":");
Serial.print(now.second());
String ampm = (now.hour() < 12) ? " AM" : " PM";
Serial.print(ampm);
Serial.print(" (");
switch(now.dayOfTheWeek()) {
case 0: Serial.print("Sunday"); break;
case 1: Serial.print("Monday"); break;
case 2: Serial.print("Tuesday"); break;
case 3: Serial.print("Wednesday"); break;
case 4: Serial.print("Thursday"); break;
case 5: Serial.print("Friday"); break;
case 6: Serial.print("Saturday"); break;
}
Serial.println(")");
Serial.println("+---------------------------------------+");
}
void updateClockDisplay() {
DateTime now = rtc.now();
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 0);
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
lcd.print(timeStr);
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
char dateStr[11];
sprintf(dateStr, "%02d/%02d/%04d", now.day(), now.month(), now.year());
lcd.print(dateStr);
}
void showMainMenu() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("1.Enroll 2.Delete");
lcd.setCursor(0, 1);
lcd.print("3.Att 4.SetTime");
Serial.println("\n=========================================");
Serial.println(" FINGERPRINT ATTENDANCE SYSTEM");
Serial.println("=========================================");
if (wifiConnected) {
Serial.println("WiFi Status: CONNECTED");
} else {
Serial.println("WiFi Status: OFFLINE MODE");
}
Serial.println("=========================================");
Serial.println(" 1. Enroll New Fingerprint");
Serial.println(" 2. Delete Fingerprint");
Serial.println(" 3. Start Attendance Mode");
Serial.println(" 4. Set RTC Time (Professor Only)");
Serial.println(" 5. Clear Attendance Records");
Serial.println("=========================================");
Serial.print("\nEnter your choice (1-5): ");
}
void handleMainMenu(String input) {
if (input == "1") {
currentMode = 1;
enrollStep = 0;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Enroll Mode");
lcd.setCursor(0, 1);
lcd.print("Type: S/P");
Serial.println("\n=========================================");
Serial.println(" ENROLLMENT MODE");
Serial.println("=========================================");
Serial.println("Select fingerprint type:");
Serial.println("S - STUDENT");
Serial.println("P - PROFESSOR");
Serial.println("Type 'QUIT' to return to menu");
Serial.print("Choice: ");
}
else if (input == "2") {
currentMode = 2;
deleteModeActive = true;
deleteConfirmed = false;
deleteStartTime = millis();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Delete Mode");
lcd.setCursor(0, 1);
lcd.print("Place finger...");
Serial.println("\n=========================================");
Serial.println(" DELETION MODE");
Serial.println("=========================================");
Serial.println("Place finger on sensor to delete");
Serial.println("Type 'QUIT' to return to menu");
}
else if (input == "3") {
if (!attendanceActive) {
currentMode = 3;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Authenticating");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
Serial.println("\n=========================================");
Serial.println(" ATTENDANCE MODE");
Serial.println("=========================================");
Serial.println("Please authenticate with Professor fingerprint");
Serial.println("Place finger on sensor...");
}
}
else if (input == "4") {
currentMode = 4;
timeSetModeActive = true;
timeSetAuthenticated = false;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Set RTC Time");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
Serial.println("\n=========================================");
Serial.println(" SET RTC TIME");
Serial.println("=========================================");
Serial.println("Professor authentication required to set time.");
Serial.println("Place finger on sensor...");
}
else if (input == "5") {
currentMode = 5;
clearModeActive = true;
clearConfirmed = false;
clearStartTime = millis();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Clear Records");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
Serial.println("\n=========================================");
Serial.println(" CLEAR ATTENDANCE RECORDS");
Serial.println("=========================================");
Serial.println("WARNING: This will delete ALL attendance records!");
Serial.println("Fingerprints and user data will remain unchanged.");
Serial.println("Please authenticate with Professor fingerprint to proceed.");
Serial.println("Place finger on sensor...");
}
else {
Serial.println("Invalid choice! Please enter 1, 2, 3, 4, or 5");
}
}
// Core Google Sheets function
void sendToGoogleSheets(String message, String value) {
if (!wifiConnected || WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected. Cannot upload");
return;
}
HTTPClient http;
// URL encode both parameters
String encodedMessage = urlEncode(message);
String encodedValue = urlEncode(value);
String url = String(googleScriptUrl) +
"?message=" + encodedMessage +
"&value=" + encodedValue;
Serial.println("Sending to Google Sheets...");
http.begin(url);
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(5000);
int httpCode = http.GET();
if (httpCode == 200) {
Serial.println("SUCCESS! Data sent to Google Sheets");
} else {
Serial.print("FAILED! HTTP Error: ");
Serial.println(httpCode);
}
http.end();
}
// Send Student Attendance (Non-blocking version)
void sendToGoogleSheetsNonBlocking(String studentId, String name, String date, String time) {
if (!wifiConnected || WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected");
return;
}
isUploading = true;
uploadStartTime = millis();
pendingStudentId = studentId;
pendingName = name;
pendingDate = date;
pendingTime = time;
}
void processPendingUpload() {
if (!isUploading) return;
if (millis() - uploadStartTime < 100) return;
// Message: "Student: Name - ID: StudentID"
// Value: "Date Time" (not used by script, but kept for consistency)
String message = "Student: " + pendingName + " - ID: " + pendingStudentId;
String value = pendingDate + " " + pendingTime;
sendToGoogleSheets(message, value);
isUploading = false;
pendingStudentId = "";
pendingName = "";
pendingDate = "";
pendingTime = "";
}
// Send Session Start (Professor)
void sendSessionStartToGoogleSheets() {
DateTime now = rtc.now();
char dateStr[11];
sprintf(dateStr, "%02d/%02d/%04d", now.day(), now.month(), now.year());
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
String dateString = String(dateStr);
String timeString = String(timeStr);
// Message: "Session Started"
// Value: "ProfessorName - Date Time - ID: SessionID"
String message = "Session Started";
String value = professorName + " - " + dateString + " " + timeString + " - ID: " + currentSessionId;
sendToGoogleSheets(message, value);
}
// Send Session End
void sendSessionEndToGoogleSheets() {
DateTime now = rtc.now();
char dateStr[11];
sprintf(dateStr, "%02d/%02d/%04d", now.day(), now.month(), now.year());
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
String dateString = String(dateStr);
String timeString = String(timeStr);
// Message: "Session Ended"
// Value: (not used, but keep format)
String message = "Session Ended";
String value = dateString + " " + timeString;
sendToGoogleSheets(message, value);
}
void handleTimeSetMode() {
if (!timeSetModeActive) return;
if (!timeSetAuthenticated) {
int p = finger.getImage();
if (p == FINGERPRINT_OK) {
p = finger.image2Tz();
if (p == FINGERPRINT_OK) {
p = finger.fingerFastSearch();
if (p == FINGERPRINT_OK) {
String userType = getUserTypeByFPId(finger.fingerID);
if (userType == "PROFESSOR") {
timeSetAuthenticated = true;
Serial.println("\nProfessor authenticated successfully!");
Serial.println("Enter date and time in format:");
Serial.println("YYYY-MM-DD HH:MM:SS");
Serial.println("Example: 2026-04-02 14:30:00");
Serial.print("Enter: ");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Professor Auth");
lcd.setCursor(0, 1);
lcd.print("Enter Time");
} else {
Serial.println("Not a professor! Access denied");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Access Denied!");
lcd.setCursor(0, 1);
lcd.print("Professor only");
delay(2000);
timeSetModeActive = false;
currentMode = 0;
showMainMenu();
}
} else {
Serial.println("Fingerprint not recognized!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Not Recognized");
lcd.setCursor(0, 1);
lcd.print("Try Again");
delay(2000);
}
}
} else if (p == FINGERPRINT_NOFINGER) {
static unsigned long lastWaitingMsg = 0;
if (millis() - lastWaitingMsg > 5000) {
lastWaitingMsg = millis();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Set RTC Time");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
}
}
}
// Handle time input after authentication
if (timeSetAuthenticated && Serial.available()) {
String timeInput = Serial.readStringUntil('\n');
timeInput.trim();
// Parse time string: "2026-04-02 14:30:00"
if (timeInput.length() >= 19) {
int year = timeInput.substring(0, 4).toInt();
int month = timeInput.substring(5, 7).toInt();
int day = timeInput.substring(8, 10).toInt();
int hour = timeInput.substring(11, 13).toInt();
int minute = timeInput.substring(14, 16).toInt();
int second = timeInput.substring(17, 19).toInt();
if (year >= 2024 && year <= 2030 && month >= 1 && month <= 12 && day >= 1 && day <= 31 &&
hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 && second >= 0 && second <= 59) {
rtc.adjust(DateTime(year, month, day, hour, minute, second));
Serial.println("Time set successfully!");
displayCurrentTime();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Time Set!");
lcd.setCursor(0, 1);
lcd.print("Success");
delay(2000);
} else {
Serial.println("Invalid time values! Please try again.");
Serial.println("Format: YYYY-MM-DD HH:MM:SS");
Serial.print("Enter: ");
return;
}
} else {
Serial.println("Invalid format! Use: YYYY-MM-DD HH:MM:SS");
Serial.print("Enter: ");
return;
}
timeSetModeActive = false;
currentMode = 0;
showMainMenu();
}
// Timeout after 30 seconds
if (timeSetModeActive && !timeSetAuthenticated && (millis() - clearStartTime > 30000)) {
Serial.println("\nTimeout: Returning to main menu");
timeSetModeActive = false;
currentMode = 0;
showMainMenu();
}
}
void handleEnrollment(String input) {
if (input.equalsIgnoreCase("QUIT")) {
currentMode = 0;
enrollStep = 0;
showMainMenu();
return;
}
if (enrollStep == 0) {
if (input.equalsIgnoreCase("S")) {
currentEnrollType = "STUDENT";
enrollStep = 1;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Enroll STUDENT");
lcd.setCursor(0, 1);
lcd.print("Enter Student ID");
Serial.print("Enter Student ID (e.g., M00814431): ");
}
else if (input.equalsIgnoreCase("P")) {
if (isProfessorExists()) {
Serial.println("Professor already enrolled!");
Serial.println("Only one professor allowed in the system");
Serial.println("Enrollment cancelled");
enrollStep = 0;
currentMode = 0;
showMainMenu();
return;
}
currentEnrollType = "PROFESSOR";
enrollStep = 2;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Enroll PROFESSOR");
lcd.setCursor(0, 1);
lcd.print("Enter Full Name");
Serial.print("Enter Professor's Full Name: ");
}
else {
Serial.println("Invalid! Enter 'S' for STUDENT or 'P' for PROFESSOR");
}
}
else if (enrollStep == 1) {
currentEnrollStudentId = input;
if (isStudentIdUsed(currentEnrollStudentId)) {
Serial.print("Student ID ");
Serial.print(currentEnrollStudentId);
Serial.println(" is already registered!");
Serial.print("Enter Student ID: ");
return;
}
enrollStep = 2;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("ID: ");
lcd.print(currentEnrollStudentId.substring(0, 12));
lcd.setCursor(0, 1);
lcd.print("Enter Name");
Serial.print("Enter full name for ");
Serial.print(currentEnrollStudentId);
Serial.print(": ");
}
else if (enrollStep == 2) {
currentEnrollName = input;
currentEnrollFPId = nextAvailableFPId;
Serial.print("Assigned internal ID: ");
Serial.println(currentEnrollFPId);
Serial.println("Ready to enroll fingerprint...");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Enrolling...");
lcd.setCursor(0, 1);
lcd.print("Place finger");
delay(1000);
performEnrollment();
}
}
void performEnrollment() {
if (enrollFingerprint(currentEnrollFPId)) {
saveUserData(currentEnrollStudentId, currentEnrollName, currentEnrollType, currentEnrollFPId);
Serial.println("\nENROLLMENT SUCCESSFUL");
Serial.print("Type: ");
Serial.println(currentEnrollType);
if (currentEnrollType == "STUDENT") {
Serial.print("Student ID: ");
Serial.println(currentEnrollStudentId);
}
Serial.print("Name: ");
Serial.println(currentEnrollName);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Success!");
lcd.setCursor(0, 1);
lcd.print(currentEnrollName.substring(0, 14));
delay(3000);
findNextAvailableFPId();
if (currentEnrollType == "PROFESSOR") {
professorName = currentEnrollName;
}
} else {
Serial.println("\nENROLLMENT FAILED");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Enrollment");
lcd.setCursor(0, 1);
lcd.print("Failed!");
delay(3000);
}
currentEnrollType = "";
currentEnrollStudentId = "";
currentEnrollName = "";
currentEnrollFPId = 0;
enrollStep = 0;
currentMode = 0;
showMainMenu();
}
bool enrollFingerprint(uint8_t fpId) {
int p = -1;
Serial.println("\nStep 1: Place finger on sensor");
while (p != FINGERPRINT_OK) {
p = finger.getImage();
switch (p) {
case FINGERPRINT_OK:
Serial.println("Image taken");
break;
case FINGERPRINT_NOFINGER:
Serial.print(".");
delay(100);
break;
case FINGERPRINT_PACKETRECIEVEERR:
Serial.println("Communication error");
return false;
case FINGERPRINT_IMAGEFAIL:
Serial.println("Imaging error");
return false;
}
}
p = finger.image2Tz(1);
if (p != FINGERPRINT_OK) {
Serial.println("Failed to convert image");
return false;
}
Serial.println("First image converted");
Serial.println("Step 2: Remove finger");
delay(2000);
p = 0;
while (p != FINGERPRINT_NOFINGER) {
p = finger.getImage();
}
Serial.println("Finger removed");
Serial.println("Step 3: Place same finger again");
p = -1;
while (p != FINGERPRINT_OK) {
p = finger.getImage();
switch (p) {
case FINGERPRINT_OK:
Serial.println("Second image taken");
break;
case FINGERPRINT_NOFINGER:
Serial.print(".");
delay(100);
break;
}
}
p = finger.image2Tz(2);
if (p != FINGERPRINT_OK) {
Serial.println("Failed to convert second image");
return false;
}
Serial.println("Second image converted");
p = finger.createModel();
if (p != FINGERPRINT_OK) {
if (p == FINGERPRINT_ENROLLMISMATCH)
Serial.println("Fingerprints did not match");
else
Serial.println("Failed to create model");
return false;
}
Serial.println("Fingerprints matched");
p = finger.storeModel(fpId);
if (p != FINGERPRINT_OK) {
Serial.println("Failed to store fingerprint");
return false;
}
Serial.println("Fingerprint stored successfully!");
return true;
}
void startAttendance() {
attendanceActive = true;
attendanceStartTime = millis();
attendanceEndTime = attendanceStartTime + (attendanceDuration * 60 * 1000);
remainingSeconds = attendanceDuration * 60;
sessionCount = 0;
// Generate unique session ID
DateTime now = rtc.now();
char sessionIdStr[20];
sprintf(sessionIdStr, "%04d%02d%02d_%02d%02d%02d",
now.year(), now.month(), now.day(),
now.hour(), now.minute(), now.second());
currentSessionId = String(sessionIdStr);
Serial.println("\nATTENDANCE SESSION STARTED");
Serial.print("Session ID: ");
Serial.println(currentSessionId);
Serial.print("Duration: ");
Serial.print(attendanceDuration);
Serial.println(" minutes");
Serial.println("Students can now place their fingers");
Serial.println("Professor can place finger again to end early");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Attendance ON");
updateCountdownDisplay();
// Send session start to Google Sheets
sendSessionStartToGoogleSheets();
}
void updateCountdownDisplay() {
if (!attendanceActive) return;
int minutes = remainingSeconds / 60;
int seconds = remainingSeconds % 60;
lcd.setCursor(0, 1);
lcd.print("Time Left: ");
if (minutes < 10) lcd.print("0");
lcd.print(minutes);
lcd.print(":");
if (seconds < 10) lcd.print("0");
lcd.print(seconds);
}
void stopAttendance() {
attendanceActive = false;
professorAuthenticated = false;
Serial.println("\nATTENDANCE SESSION ENDED");
Serial.println("\n+---------------------------------------+");
Serial.println("| ATTENDANCE SUMMARY |");
Serial.println("+---------------------------------------+");
Serial.print("| Total Students Present: ");
Serial.print(sessionCount);
for(int i = 0; i < 10; i++) Serial.print(" ");
Serial.println("|");
Serial.println("+---------------------------------------+");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Session Ended");
lcd.setCursor(0, 1);
lcd.print("Students: ");
lcd.print(sessionCount);
delay(3000);
// Send session end to Google Sheets
sendSessionEndToGoogleSheets();
currentMode = 0;
showMainMenu();
}
void markAttendance(String studentId, String name) {
DateTime now = rtc.now();
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
char dateStr[11];
sprintf(dateStr, "%02d/%02d/%04d", now.day(), now.month(), now.year());
for (int i = 0; i < sessionCount; i++) {
if (currentSession[i].studentId == studentId) {
Serial.print("\n");
Serial.print(name);
Serial.println(" already marked attendance in this session!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Already Marked");
lcd.setCursor(0, 1);
lcd.print(name.substring(0, 15));
delay(2000);
return;
}
}
if (isAttendanceRecorded(studentId, dateStr)) {
Serial.print("\n");
Serial.print(name);
Serial.println(" already marked attendance today!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Already Marked");
lcd.setCursor(0, 1);
lcd.print(name.substring(0, 15));
delay(2000);
return;
}
currentSession[sessionCount].studentId = studentId;
currentSession[sessionCount].name = name;
currentSession[sessionCount].marked = true;
sessionCount++;
saveAttendanceLog(studentId, name, timeStr, dateStr);
// Non-blocking upload to Google Sheets
sendToGoogleSheetsNonBlocking(studentId, name, dateStr, timeStr);
// Serial output without IP address
Serial.println("\n+---------------------------------------+");
Serial.println("| ATTENDANCE RECORDED |");
Serial.println("+---------------------------------------+");
Serial.print("| Date: ");
Serial.print(dateStr);
Serial.print(" | Time: ");
Serial.println(timeStr);
Serial.print("| Student ID: ");
Serial.print(studentId);
for(int i = studentId.length(); i < 19; i++) Serial.print(" ");
Serial.println("|");
Serial.print("| Name: ");
Serial.print(name);
for(int i = name.length(); i < 25; i++) Serial.print(" ");
Serial.println("|");
Serial.println("+---------------------------------------+");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("ID: ");
lcd.print(studentId.substring(0, 12));
lcd.setCursor(0, 1);
lcd.print("Name: ");
lcd.print(name.substring(0, 11));
delay(3000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Attendance ON");
updateCountdownDisplay();
}
void handleClearRecordsMode() {
if (!clearModeActive) return;
if (!clearConfirmed) {
int p = finger.getImage();
if (p == FINGERPRINT_OK) {
p = finger.image2Tz();
if (p == FINGERPRINT_OK) {
p = finger.fingerFastSearch();
if (p == FINGERPRINT_OK) {
String userType = getUserTypeByFPId(finger.fingerID);
if (userType == "PROFESSOR") {
Serial.println("\nProfessor authenticated successfully!");
Serial.println("WARNING: This will delete ALL attendance records!");
Serial.print("Type 'CONFIRM' to proceed or anything else to cancel: ");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Professor Auth");
lcd.setCursor(0, 1);
lcd.print("Confirm? CONFIRM");
clearConfirmed = true;
clearStartTime = millis();
} else {
Serial.println("Not a professor! Access denied");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Access Denied!");
lcd.setCursor(0, 1);
lcd.print("Professor only");
delay(2000);
clearModeActive = false;
currentMode = 0;
showMainMenu();
}
} else {
Serial.println("Fingerprint not recognized!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Not Recognized");
lcd.setCursor(0, 1);
lcd.print("Try Again");
delay(2000);
}
}
} else if (p == FINGERPRINT_NOFINGER) {
static unsigned long lastWaitingMsg = 0;
if (millis() - lastWaitingMsg > 5000) {
lastWaitingMsg = millis();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Clear Records");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
}
}
}
if (clearConfirmed && Serial.available()) {
String input = Serial.readStringUntil('\n');
input.trim();
if (input == "CONFIRM") {
if (SPIFFS.exists("/attendance.txt")) {
if (SPIFFS.remove("/attendance.txt")) {
Serial.println("\nAttendance records deleted");
} else {
Serial.println("Failed to delete attendance records");
}
}
logCount = 0;
for (int i = 0; i < 500; i++) {
logs[i].studentId = "";
logs[i].name = "";
logs[i].timestamp = "";
logs[i].date = "";
}
sessionCount = 0;
for (int i = 0; i < 128; i++) {
currentSession[i].studentId = "";
currentSession[i].name = "";
currentSession[i].marked = false;
}
Serial.println("\nALL ATTENDANCE RECORDS CLEARED");
Serial.println("Fingerprints and user data remain unchanged.");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Records Cleared");
lcd.setCursor(0, 1);
lcd.print("Success!");
delay(3000);
} else {
Serial.println("\nOperation cancelled. Attendance records preserved.");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Cancelled");
lcd.setCursor(0, 1);
lcd.print("Records Kept");
delay(2000);
}
clearModeActive = false;
currentMode = 0;
showMainMenu();
}
if (clearModeActive && !clearConfirmed && (millis() - clearStartTime > 30000)) {
Serial.println("\nTimeout: Returning to main menu");
clearModeActive = false;
currentMode = 0;
showMainMenu();
}
}
void handleAttendanceMode() {
if (!attendanceActive) {
int p = finger.getImage();
if (p == FINGERPRINT_OK) {
p = finger.image2Tz();
if (p == FINGERPRINT_OK) {
p = finger.fingerFastSearch();
if (p == FINGERPRINT_OK) {
String userType = getUserTypeByFPId(finger.fingerID);
if (userType == "PROFESSOR") {
startAttendance();
} else {
Serial.println("Not a professor! Access denied");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Access Denied!");
lcd.setCursor(0, 1);
lcd.print("Professor only");
delay(2000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Authenticating");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
}
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Not Recognized");
lcd.setCursor(0, 1);
lcd.print("Try Again");
delay(1500);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Authenticating");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
}
}
} else if (p == FINGERPRINT_NOFINGER) {
static unsigned long lastWaitingMsg = 0;
if (millis() - lastWaitingMsg > 5000) {
lastWaitingMsg = millis();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Authenticating");
lcd.setCursor(0, 1);
lcd.print("Place Professor FP");
}
}
} else {
unsigned long currentTime = millis();
if (currentTime >= attendanceEndTime) {
stopAttendance();
return;
}
// Update remaining seconds (timer continues even during upload)
remainingSeconds = (attendanceEndTime - currentTime) / 1000;
updateCountdownDisplay();
// Process any pending upload (non-blocking)
processPendingUpload();
static unsigned long lastScan = 0;
if (millis() - lastScan > 500) {
lastScan = millis();
int p = finger.getImage();
if (p == FINGERPRINT_OK) {
p = finger.image2Tz();
if (p == FINGERPRINT_OK) {
p = finger.fingerFastSearch();
if (p == FINGERPRINT_OK) {
String userType = getUserTypeByFPId(finger.fingerID);
if (userType == "PROFESSOR") {
Serial.println("\nProfessor ending attendance session early");
stopAttendance();
return;
} else if (userType == "STUDENT") {
String studentId = getStudentIdByFPId(finger.fingerID);
String name = getNameByFPId(finger.fingerID);
markAttendance(studentId, name);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Attendance ON");
updateCountdownDisplay();
}
}
}
}
}
}
}
void handleDeleteMode() {
if (!deleteModeActive) return;
if (!deleteConfirmed) {
int p = finger.getImage();
if (p == FINGERPRINT_OK) {
p = finger.image2Tz();
if (p == FINGERPRINT_OK) {
p = finger.fingerFastSearch();
if (p == FINGERPRINT_OK) {
deleteFPId = finger.fingerID;
for (int i = 0; i < userCount; i++) {
if (users[i].fpId == deleteFPId && users[i].active) {
deleteStudentId = users[i].studentId;
deleteName = users[i].name;
break;
}
}
Serial.print("\nFingerprint recognized!\n");
Serial.print("Student ID: ");
Serial.println(deleteStudentId);
Serial.print("Name: ");
Serial.println(deleteName);
Serial.print("Type: ");
Serial.println(getUserTypeByFPId(deleteFPId));
Serial.print("\nConfirm deletion of ");
Serial.print(deleteName);
Serial.println("? (Y/N): ");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Delete: ");
lcd.print(deleteName.substring(0, 12));
lcd.setCursor(0, 1);
lcd.print("Confirm? Y/N");
deleteConfirmed = true;
deleteStartTime = millis();
} else {
Serial.println("Fingerprint not found in database!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Not Found!");
lcd.setCursor(0, 1);
lcd.print("Try Again");
delay(2000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Delete Mode");
lcd.setCursor(0, 1);
lcd.print("Place finger...");
}
}
} else if (p == FINGERPRINT_NOFINGER) {
static unsigned long lastWaitingMsg = 0;
if (millis() - lastWaitingMsg > 5000) {
lastWaitingMsg = millis();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Delete Mode");
lcd.setCursor(0, 1);
lcd.print("Place finger...");
}
}
}
if (deleteConfirmed && Serial.available()) {
String input = Serial.readStringUntil('\n');
input.trim();
if (input.equalsIgnoreCase("Y")) {
uint8_t p = finger.deleteModel(deleteFPId);
if (p == FINGERPRINT_OK) {
deleteUserData(deleteStudentId);
Serial.print("\nDELETED\n");
Serial.print("Student ID: ");
Serial.println(deleteStudentId);
Serial.print("Name: ");
Serial.println(deleteName);
Serial.println("has been deleted");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Deleted!");
lcd.setCursor(0, 1);
lcd.print(deleteName.substring(0, 14));
delay(3000);
findNextAvailableFPId();
} else {
Serial.println("Deletion failed!");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Delete Failed!");
delay(2000);
}
deleteModeActive = false;
currentMode = 0;
showMainMenu();
}
else if (input.equalsIgnoreCase("N") || input.equalsIgnoreCase("QUIT")) {
Serial.println("Deletion cancelled");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Cancelled");
delay(2000);
deleteModeActive = false;
currentMode = 0;
showMainMenu();
}
}
if (deleteModeActive && !deleteConfirmed && (millis() - deleteStartTime > 30000)) {
Serial.println("\nTimeout: Returning to main menu");
deleteModeActive = false;
currentMode = 0;
showMainMenu();
}
}
// Data Management Functions
bool isStudentIdUsed(String studentId) {
for (int i = 0; i < userCount; i++) {
if (users[i].studentId == studentId && users[i].active) {
return true;
}
}
return false;
}
bool isProfessorExists() {
for (int i = 0; i < userCount; i++) {
if (users[i].type == "PROFESSOR" && users[i].active) {
return true;
}
}
return false;
}
String getNameByFPId(uint8_t fpId) {
for (int i = 0; i < userCount; i++) {
if (users[i].fpId == fpId && users[i].active) {
return users[i].name;
}
}
return "Unknown";
}
String getStudentIdByFPId(uint8_t fpId) {
for (int i = 0; i < userCount; i++) {
if (users[i].fpId == fpId && users[i].active) {
if (users[i].type == "STUDENT") {
return users[i].studentId;
} else {
return "PROFESSOR";
}
}
}
return "Unknown";
}
String getUserTypeByFPId(uint8_t fpId) {
for (int i = 0; i < userCount; i++) {
if (users[i].fpId == fpId && users[i].active) {
return users[i].type;
}
}
return "UNKNOWN";
}
void findNextAvailableFPId() {
nextAvailableFPId = 1;
bool used[128] = {false};
for (int i = 0; i < userCount; i++) {
if (users[i].active && users[i].fpId > 0 && users[i].fpId < 128) {
used[users[i].fpId] = true;
}
}
for (int i = 1; i < 128; i++) {
if (!used[i]) {
nextAvailableFPId = i;
break;
}
}
}
void saveUserData(String studentId, String name, String type, uint8_t fpId) {
users[userCount].studentId = studentId;
users[userCount].name = name;
users[userCount].type = type;
users[userCount].fpId = fpId;
users[userCount].active = true;
userCount++;
File file = SPIFFS.open("/users.txt", FILE_APPEND);
if (file) {
file.print(studentId);
file.print(",");
file.print(name);
file.print(",");
file.print(type);
file.print(",");
file.println(fpId);
file.close();
}
}
void deleteUserData(String studentId) {
for (int i = 0; i < userCount; i++) {
if (users[i].studentId == studentId) {
users[i].active = false;
break;
}
}
File file = SPIFFS.open("/users.txt", "w");
if (file) {
for (int i = 0; i < userCount; i++) {
if (users[i].active) {
file.print(users[i].studentId);
file.print(",");
file.print(users[i].name);
file.print(",");
file.print(users[i].type);
file.print(",");
file.println(users[i].fpId);
}
}
file.close();
}
}
void loadUserData() {
userCount = 0;
if (SPIFFS.exists("/users.txt")) {
File file = SPIFFS.open("/users.txt", "r");
if (file) {
while (file.available() && userCount < 128) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
int comma1 = line.indexOf(',');
int comma2 = line.indexOf(',', comma1 + 1);
int comma3 = line.indexOf(',', comma2 + 1);
if (comma1 > 0 && comma2 > 0 && comma3 > 0) {
users[userCount].studentId = line.substring(0, comma1);
users[userCount].name = line.substring(comma1 + 1, comma2);
users[userCount].type = line.substring(comma2 + 1, comma3);
users[userCount].fpId = line.substring(comma3 + 1).toInt();
users[userCount].active = true;
userCount++;
}
}
}
file.close();
}
}
}
void saveAttendanceLog(String studentId, String name, String time, String date) {
if (isAttendanceRecorded(studentId, date)) return;
logs[logCount].studentId = studentId;
logs[logCount].name = name;
logs[logCount].timestamp = time;
logs[logCount].date = date;
logCount++;
File file = SPIFFS.open("/attendance.txt", FILE_APPEND);
if (file) {
file.print(studentId);
file.print(",");
file.print(name);
file.print(",");
file.print(time);
file.print(",");
file.println(date);
file.close();
}
}
bool isAttendanceRecorded(String studentId, String date) {
for (int i = 0; i < logCount; i++) {
if (logs[i].studentId == studentId && logs[i].date == date) {
return true;
}
}
if (SPIFFS.exists("/attendance.txt")) {
File file = SPIFFS.open("/attendance.txt", "r");
if (file) {
while (file.available()) {
String line = file.readStringUntil('\n');
if (line.indexOf(studentId) == 0 && line.indexOf(date) > 0) {
file.close();
return true;
}
}
file.close();
}
}
return false;
}
void loadAttendanceLogs() {
logCount = 0;
if (SPIFFS.exists("/attendance.txt")) {
File file = SPIFFS.open("/attendance.txt", "r");
if (file) {
while (file.available() && logCount < 500) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
int comma1 = line.indexOf(',');
int comma2 = line.indexOf(',', comma1 + 1);
int comma3 = line.indexOf(',', comma2 + 1);
if (comma1 > 0 && comma2 > 0 && comma3 > 0) {
logs[logCount].studentId = line.substring(0, comma1);
logs[logCount].name = line.substring(comma1 + 1, comma2);
logs[logCount].timestamp = line.substring(comma2 + 1, comma3);
logs[logCount].date = line.substring(comma3 + 1);
logCount++;
}
}
}
file.close();
}
}
}
void loop() {
Comments
Post a Comment