Rohan Barnwal
Published © GPL3+

Tap-In Attendance: Apple Wallet & RFID Classroom System

Teacher taps iPhone to start/end class; students tap RFID cards to check in — REYAX RYRR30D logs it all live to Google Sheets.

BeginnerShowcase (no instructions)1 hour47
Tap-In Attendance: Apple Wallet & RFID Classroom System

Things used in this project

Hardware components

RYRR30D
×1
0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
×1
Arduino Uno R4 WiFi
Arduino Uno R4 WiFi
×1
Jumper wires (generic)
Jumper wires (generic)
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Code

Code

Arduino
#include <WiFiS3.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

const char* host = "script.google.com";
const int httpsPort = 443;
String GScriptId = "YOUR_APPS_SCRIPT_DEPLOYMENT_ID";

WiFiSSLClient client;

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

const String TEACHER_PASS = "Reyax|RYRR30DTest|88880000";

struct Student {
  String uid;
  String name;
};

Student students[] = {
  {"D784D500", "Student 1"},
  {"7EB6F300", "Student 2"},
  {"23370EAA", "Student 3"},
  {"827ED600", "Student 4"},
  {"E2D2D500", "Student 5"},
  {"D7CFE000", "Student 6"},
  {"310AF400", "Student 7"}
};
const int numStudents = 7;

enum SystemState { WAITING_FOR_TEACHER, SETTLING, IN_SESSION };
SystemState currentState = WAITING_FOR_TEACHER;

unsigned long lastConfirmedApple = 0;
unsigned long lastCardTime = 0;
String lastCardUID = "";

const unsigned long APPLE_DEBOUNCE = 6000;
const unsigned long CARD_DEBOUNCE = 3000;

unsigned long settleStartTime = 0;
const unsigned long SETTLE_TIME = 3500;

bool hasPendingCard = false;
String pendingUID = "";
unsigned long pendingCardTime = 0;
const unsigned long HOLD_WINDOW = 1300;

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200);

  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println("OLED not found!");
    while (true);
  }

  showMessage("Welcome To\nSchool");
  delay(2000);

  showMessage("Connecting\nWiFi...");
  WiFi.begin(ssid, password);
  int tries = 0;
  while (WiFi.status() != WL_CONNECTED && tries < 20) {
    delay(500);
    tries++;
  }
  if (WiFi.status() == WL_CONNECTED) {
    showMessage("WiFi\nConnected!");
  } else {
    showMessage("WiFi\nFailed!");
  }
  delay(1500);

  // Configure RYRR30D with the authorized Apple Wallet key/ID
  Serial1.print("AT+APPLE=1,60e9798a4d98f0fd86ed5ef0eb459ffb72154ad47694bbc89f9b352ba3ad739d,7dea8e42245bc14db7c2e359218fca2f4566a68eac704c29ec3286fa063e2cea\r\n");
  delay(200);
  while (Serial1.available()) Serial1.read();

  Serial1.print("AT+MODE=2\r\n");
  delay(200);
  while (Serial1.available()) Serial1.read();

  showMessage("Waiting For\nTeacher");
}

void loop() {
  unsigned long now = millis();

  if (Serial1.available()) {
    String data = Serial1.readStringUntil('\n');
    data.trim();

    if (data.length() > 0) {
      // ---- APPLE (Teacher iPhone) ----
      if (data.startsWith("+APPLE=")) {
        String payload = data.substring(data.indexOf(',') + 1);

        if (hasPendingCard) {
          hasPendingCard = false; // discard — this was a phantom from the same tap
        }

        if (payload == TEACHER_PASS && (now - lastConfirmedApple > APPLE_DEBOUNCE)) {
          lastConfirmedApple = now;

          delay(150);
          while (Serial1.available()) Serial1.read(); // flush leftover phantoms

          if (currentState == WAITING_FOR_TEACHER) {
            drawTickSimple();
            delay(1000);
            currentState = SETTLING;
            settleStartTime = now;
            showMessage("Remove\nPhone");
          }
          else if (currentState == SETTLING || currentState == IN_SESSION) {
            currentState = WAITING_FOR_TEACHER;
            drawDoneScreen();
            delay(2500);
            showMessage("Waiting For\nTeacher");
          }
        }
      }
      // ---- ISO14443A (Student Cards) ----
      else if (data.startsWith("+ISO14443A=")) {
        if (currentState == SETTLING) {
          // ignore unconditionally during settle window
        }
        else if (currentState == IN_SESSION) {
          String uid = data.substring(data.indexOf('=') + 1);

          if (!(uid == lastCardUID && (now - lastCardTime < CARD_DEBOUNCE))) {
            pendingUID = uid;
            pendingCardTime = now;
            hasPendingCard = true;
          }
        }
      }
    }
  }

  if (currentState == SETTLING && (now - settleStartTime >= SETTLE_TIME)) {
    currentState = IN_SESSION;
    showMessage("Tap Student\nCards Now");
  }

  if (hasPendingCard && (now - pendingCardTime >= HOLD_WINDOW)) {
    hasPendingCard = false;
    lastCardUID = pendingUID;
    lastCardTime = now;

    String studentName = getStudentName(pendingUID);

    if (studentName != "") {
      drawTickSimple();
      sendToGoogleSheet(pendingUID, studentName, "Present");
    } else {
      drawCrossSimple();
      sendToGoogleSheet(pendingUID, "Unknown", "Invalid");
    }
    delay(1200);
    showMessage("Tap Student\nCards Now");
  }
}

void sendToGoogleSheet(String uid, String name, String status) {
  if (WiFi.status() != WL_CONNECTED) return;

  if (client.connect(host, httpsPort)) {
    String url = "/macros/s/" + GScriptId + "/exec?uid=" + urlEncode(uid) +
                 "&name=" + urlEncode(name) + "&status=" + urlEncode(status);

    client.println("GET " + url + " HTTP/1.1");
    client.println("Host: " + String(host));
    client.println("Connection: close");
    client.println();

    unsigned long timeout = millis();
    while (client.connected() && millis() - timeout < 5000) {
      if (client.available()) {
        String line = client.readStringUntil('\n');
        if (line == "\r") break;
      }
    }
    client.stop();
  }
}

String urlEncode(String str) {
  String encoded = "";
  char c, code0, code1;
  for (int i = 0; i < str.length(); i++) {
    c = str.charAt(i);
    if (isalnum(c)) {
      encoded += c;
    } else {
      code1 = (c & 0xf) + '0';
      if ((c & 0xf) > 9) code1 = (c & 0xf) - 10 + 'A';
      c = (c >> 4) & 0xf;
      code0 = c + '0';
      if (c > 9) code0 = c - 10 + 'A';
      encoded += '%';
      encoded += code0;
      encoded += code1;
    }
  }
  return encoded;
}

String getStudentName(String uid) {
  for (int i = 0; i < numStudents; i++) {
    if (uid == students[i].uid) return students[i].name;
  }
  return "";
}

void showMessage(String msg) {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 10);
  display.println(msg);
  display.display();
}

void drawTickSimple() {
  display.clearDisplay();
  int x1 = 25, y1 = 35, x2 = 50, y2 = 55, x3 = 100, y3 = 15;
  for (int i = 0; i <= 20; i++) {
    int xi = x1 + (x2 - x1) * i / 20, yi = y1 + (y2 - y1) * i / 20;
    display.drawLine(x1, y1, xi, yi, SSD1306_WHITE);
    display.drawLine(x1, y1 + 1, xi, yi + 1, SSD1306_WHITE);
    display.display();
    delay(12);
  }
  for (int i = 0; i <= 20; i++) {
    int xi = x2 + (x3 - x2) * i / 20, yi = y2 + (y3 - y2) * i / 20;
    display.drawLine(x2, y2, xi, yi, SSD1306_WHITE);
    display.drawLine(x2, y2 + 1, xi, yi + 1, SSD1306_WHITE);
    display.display();
    delay(12);
  }
}

void drawCrossSimple() {
  display.clearDisplay();
  int x1 = 30, y1 = 15, x2 = 95, y2 = 55, x3 = 95, y3v = 15, x4 = 30, y4 = 55;
  for (int i = 0; i <= 20; i++) {
    int xi = x1 + (x2 - x1) * i / 20, yi = y1 + (y2 - y1) * i / 20;
    display.drawLine(x1, y1, xi, yi, SSD1306_WHITE);
    display.drawLine(x1, y1 + 1, xi, yi + 1, SSD1306_WHITE);
    display.display();
    delay(10);
  }
  for (int i = 0; i <= 20; i++) {
    int xi = x3 + (x4 - x3) * i / 20, yi = y3v + (y4 - y3v) * i / 20;
    display.drawLine(x3, y3v, xi, yi, SSD1306_WHITE);
    display.drawLine(x3, y3v + 1, xi, yi + 1, SSD1306_WHITE);
    display.display();
    delay(10);
  }
}

void drawDoneScreen() {
  display.clearDisplay();
  display.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_WHITE);
  display.setTextColor(SSD1306_BLACK);
  display.setTextSize(3);
  display.setCursor(15, 22);
  display.println("DONE");
  display.display();
}

Credits

Rohan Barnwal
52 projects • 41 followers
Rohan Barnwal - maker, hacker, tech enthusiast. I explore new tech & find innovative solutions. See my projects on hackster.io!

Comments