#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <WiFi.h>
#include <HTTPClient.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Login buttons
#define BTN1 13
#define BTN2 12
#define BTN3 14
// Presence buttons
#define PRESENCE_1 27
#define PRESENCE_2 26
#define PRESENCE_3 25
#define BUZZER_PIN 23
#define LOGIN_DURATION 15000
#define EXAM_DURATION 60000
#define ABSENCE_LIMIT 20000
#define MAX_QUESTIONS 4
const char* scriptURL = "https://script.google.com/macros/s/AKfycbx8kevTD4Mm0FCYIZngwHHoAzdz4fFQUU6n7T2Vn5MByXQkYUc4wTQE88duITLSoX87XQ/exec";
// Keypad
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{ '1', '2', '3', 'A' },
{ '4', '5', '6', 'B' },
{ '7', '8', '9', 'C' },
{ '*', '0', '#', 'D' }
};
byte rowPins[ROWS] = { 18, 19, 33, 32 }; // Pins connected to R1, R2, R3, R4
byte colPins[COLS] = { 5, 17, 16, 4 }; // Pins connected to C1, C2, C3, C4
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// Student structure
struct Student {
const char* id;
const char* name;
bool loggedIn;
bool locked;
bool terminated;
unsigned long lastPresence;
};
Student students[] = {
{"161234", "Ali", false, false, false, 0},
{"165678", "Siti", false, false, false, 0},
{"160912", "Adrian", false, false, false, 0}
};
// Button to Student ID mapping
const char* buttonIDMap[] = {
"161234", // BTN1 -> Ali
"165678", // BTN2 -> Siti
"160000" // BTN3 -> Adrian
};
const int studentCount = 3;
unsigned long loginStart = 0;
unsigned long examStart = 0;
bool examStartedFlag = false;
bool examOngoing = false;
int studentMarks[studentCount] = {0};
char studentAnswers[studentCount][MAX_QUESTIONS];
int currentStudentIndex = 0;
int currentQuestion = 0;
char correctAnswers[MAX_QUESTIONS] = {'1', '2', '3', '4'};
void setup() {
Serial.begin(115200);
lcd.init();
lcd.backlight();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
pinMode(BTN1, INPUT_PULLUP);
pinMode(BTN2, INPUT_PULLUP);
pinMode(BTN3, INPUT_PULLUP);
pinMode(PRESENCE_1, INPUT_PULLUP);
pinMode(PRESENCE_2, INPUT_PULLUP);
pinMode(PRESENCE_3, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
lcd.setCursor(0, 0);
lcd.print("Login Time: 15s");
Serial.print("Connecting to WiFi");
WiFi.begin("Wokwi-GUEST", "", 6);
while (WiFi.status() != WL_CONNECTED) {
delay(100);
Serial.print(".");
}
Serial.println(" Connected!");
loginStart = millis();
}
void loop() {
if (!examStartedFlag) {
handleLogin();
} else if (!examOngoing) {
examOngoing = true;
} else {
monitorPresence();
showLCDTimer();
checkKeypadPerStudent();
if (millis() - examStart >= EXAM_DURATION || currentStudentIndex >= studentCount) {
endExam();
}
}
}
void handleLogin() {
unsigned long now = millis();
int remainingLogin = (LOGIN_DURATION - (now - loginStart)) / 1000;
lcd.setCursor(0, 1);
lcd.print("Time Left: ");
lcd.print(remainingLogin);
lcd.print("s ");
// Check button presses and attempt login
if (digitalRead(BTN1) == LOW) checkAndLogin(buttonIDMap[0]);
if (digitalRead(BTN2) == LOW) checkAndLogin(buttonIDMap[1]);
if (digitalRead(BTN3) == LOW) checkAndLogin(buttonIDMap[2]);
// If all students have logged in early
if (allLoggedIn() && !examStartedFlag) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("All logged in!");
delay(2000);
lcd.clear();
examStartedFlag = true;
for (int i = 0; i < studentCount; i++) students[i].lastPresence = millis();
lcd.setCursor(0, 0);
lcd.print("Exam Starting...");
delay(2000);
examStart = millis();
lcd.clear();
examOngoing = true;
return;
}
// If login time is over and exam hasn't started
if ((now - loginStart >= LOGIN_DURATION) && !examStartedFlag) {
terminateUnlogged();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Login time over!");
delay(1500);
examStartedFlag = true;
for (int i = 0; i < studentCount; i++) students[i].lastPresence = millis();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Exam Starting...");
delay(2000);
examStart = millis();
lcd.clear();
examOngoing = true;
}
}
void simulateRFID(const char* scannedID) {
for (int i = 0; i < studentCount; i++) {
if (!students[i].loggedIn && strcmp(scannedID, students[i].id) == 0) {
students[i].loggedIn = true;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Welcome: ");
lcd.setCursor(0, 1);
lcd.print(students[i].name);
delay(1000);
lcd.clear();
}
}
}
void checkAndLogin(const char* scannedID) {
Serial.print("Scanned ID: ");
Serial.println(scannedID);
bool authorized = false;
for (int i = 0; i < studentCount; i++) {
Serial.print("Comparing with stored ID: ");
Serial.println(students[i].id);
if (strcmp(scannedID, students[i].id) == 0) {
authorized = true;
if (!students[i].loggedIn) {
students[i].loggedIn = true;
Serial.print("Login successful for: ");
Serial.println(students[i].name);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Welcome: ");
lcd.setCursor(0, 1);
lcd.print(students[i].name);
delay(1000);
lcd.clear();
} else {
Serial.print("Already logged in: ");
Serial.println(students[i].name);
}
break;
}
}
if (!authorized) {
Serial.println("Access Denied: Unauthorized ID");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Access Denied");
delay(1000);
lcd.clear();
}
}
bool allLoggedIn() {
for (int i = 0; i < studentCount; i++) {
if (!students[i].loggedIn) return false;
}
return true;
}
void terminateUnlogged() {
for (int i = 0; i < studentCount; i++) {
if (!students[i].loggedIn) students[i].terminated = true;
}
updateOLED();
}
void monitorPresence() {
int pins[] = {PRESENCE_1, PRESENCE_2, PRESENCE_3};
for (int i = 0; i < studentCount; i++) {
if (!students[i].loggedIn || students[i].locked || students[i].terminated) continue;
if (digitalRead(pins[i]) == LOW) students[i].lastPresence = millis();
if (millis() - students[i].lastPresence > ABSENCE_LIMIT) {
students[i].locked = true;
digitalWrite(BUZZER_PIN, HIGH);
delay(300);
digitalWrite(BUZZER_PIN, LOW);
}
}
updateOLED();
}
void updateOLED() {
display.clearDisplay();
for (int i = 0; i < studentCount; i++) {
display.setCursor(0, i * 10);
display.print(students[i].name);
display.print(": ");
const char* status;
if (students[i].terminated) status = "TERMINATED";
else if (students[i].locked) status = "LOCKED";
else status = "OK";
display.print(status);
}
display.display();
}
void showLCDTimer() {
int remaining = (EXAM_DURATION - (millis() - examStart)) / 1000;
lcd.setCursor(0, 0);
lcd.print("Exam: ");
lcd.print(remaining);
lcd.print("s ");
lcd.setCursor(0, 1);
}
void checkKeypadPerStudent() {
if (currentStudentIndex >= studentCount) return;
Student& s = students[currentStudentIndex];
if (!s.loggedIn || s.locked || s.terminated) {
currentStudentIndex++;
currentQuestion = 0;
return;
}
lcd.setCursor(0, 1);
lcd.print("Student: ");
lcd.print(s.name);
lcd.setCursor(0, 2);
lcd.print("Q");
lcd.print(currentQuestion + 1);
lcd.print(" Ans: ");
char key = keypad.getKey();
if (key != NO_KEY) {
studentAnswers[currentStudentIndex][currentQuestion] = key;
Serial.printf("%s - Q%d Answered: %c\n", s.name, currentQuestion + 1, key);
currentQuestion++;
if (currentQuestion >= MAX_QUESTIONS) {
currentStudentIndex++;
currentQuestion = 0;
lcd.clear();
} else {
lcd.clear();
}
}
}
void endExam() {
lcd.clear();
for (int i = 0; i < studentCount; i++) {
if (!students[i].loggedIn || students[i].locked || students[i].terminated) {
studentMarks[i] = 0;
continue;
}
int mark = 0;
for (int q = 0; q < MAX_QUESTIONS; q++) {
if (studentAnswers[i][q] == correctAnswers[q]) {
mark += 5;
}
}
studentMarks[i] = mark;
}
for (int i = 0; i < studentCount; i++) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(students[i].name);
lcd.setCursor(0, 1);
lcd.print("Mark: ");
lcd.print(studentMarks[i]);
delay(3000); // Show for 3s each
const char* presenceStatus;
if (students[i].terminated) presenceStatus = "TERMINATED";
else if (students[i].locked) presenceStatus = "LOCKED";
else presenceStatus = "OK";
sendPresenceStatusToSheet(students[i].name, presenceStatus, studentMarks[i]);
delay(300);
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Thank You!");
while (true);
}
void sendPresenceStatusToSheet(const char* name, const char* status, int score) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
String url = String(scriptURL);
url += "?timestamp=" + String(millis() / 1000);
url += "&name=" + String(name);
url += "&status=" + String(status);
url += "&score=" + String(score);
Serial.println("[WiFi] Sending presence + score to Google Sheet...");
Serial.println(url);
http.begin(url);
int httpResponseCode = http.GET();
if (httpResponseCode > 0) {
Serial.print("Response code: ");
Serial.println(httpResponseCode);
} else {
Serial.print("HTTP GET failed, code: ");
Serial.println(httpResponseCode);
}
http.end();
} else {
Serial.println("WiFi not connected.");
}
}