Tap-In Attendance: A Teacher-Controlled, Apple Wallet & RFID Based Classroom Attendance System

A dual-mode attendance system where a teacher's iPhone starts and ends the session, and students simply tap an RFID card to be marked present — built with an Arduino UNO R4 WiFi, a REYAX RYRR30D NFC/RFID reader, an OLED display, and logged live to Google Sheets.

Overview

Classroom attendance is still, in most schools, either a shouted roll call or a sheet of paper passed down the row — slow, error-prone, and impossible to search later. This project replaces that with a small tap-based terminal: the teacher taps their iPhone (using an Apple Wallet pass) to open an attendance session, students tap their RFID cards one by one to check in, and the teacher taps their iPhone again to close the session. Every valid and invalid tap is shown instantly on an OLED display and logged — with a name, UID, status, and timestamp — directly into a Google Sheet, thanks to the Arduino UNO R4 WiFi's built-in WiFi.

At the core of the system is the REYAX RYRR30D, an NFC/RFID module officially certified to read Apple Wallet VAS (Value Added Services) passes as well as standard ISO14443A RFID cards — meaning a single reader handles both the teacher's phone-based authentication and the students' physical ID cards. The whole thing is housed in a JUSTWAY enclosure, making it a clean, mountable classroom fixture rather than a bundle of breadboard wires.

What It Does

Each student taps their RFID card:

Every event — teacher taps, student taps, valid/invalid results, WiFi status, and sheet upload confirmations — is also printed to the Serial Monitor in real time, which made debugging (and will make future maintenance) far easier.

Why RYRR30D ?

This project actually needed the RYRR30D to do two different jobs, and it handles both natively:

Without this dual capability, this project would have needed two separate reader modules — one for phone-based VAS passes and one for RFID — adding cost, wiring, and firmware complexity for no real benefit.

Learn More About the RYRR30D

For anyone who wants to understand exactly how the module decodes Apple Wallet passes, what AT commands are available, or how to issue your own Apple Merchant ID/Key pair, REYAX's official documentation is the best starting point:

The application notes in particular are worth reading before scaling this system beyond a single classroom — they cover how Apple Merchant IDs and Keys are generated and how to issue your own credentials rather than relying on REYAX's demo pass.

The Phantom Signal Problem

Early in development, a subtle but important bug showed up: when a teacher's iPhone lingers near the antenna for more than a moment, the module can emit multiple stray +ISO14443A=<random UID> events — essentially phantom "card" reads generated by the phone itself before or after the real +APPLE= event resolves. Left unhandled, these phantom reads were being mistaken for invalid student cards immediately after every teacher tap.

The fix implemented here is a three-state machine plus a deliberate settling period:

WAITING_FOR_TEACHER → SETTLING → IN_SESSION

This combination — explicit settle state + debounce + hold-and-confirm — turned out to be necessary because timing thresholds alone weren't reliable across different tap durations.

Google Sheets Logging

The UNO R4 WiFi's built-in WiFi makes cloud logging straightforward without any extra networking hardware. A Google Apps Script deployed as a web app receives simple GET requests and appends a row to a Google Sheet:

javascript

function doGet(e) {  var sheet = SpreadsheetApp      .openById("YOUR_SHEET_ID")      .getSheetByName("Attendance");
  sheet.appendRow([    e.parameter.uid,    e.parameter.name,    e.parameter.status,    new Date()  ]);
  return ContentService.createTextOutput("SUCCESS");
}

The Arduino sends an HTTPS GET request to this script's deployment URL every time a student card is read, passing the UID, resolved student name, and status ("Present" or "Invalid") as URL parameters. No third-party libraries or paid services are needed — Apps Script deployed as a public web app is free and works directly with WiFiSSLClient.

The Code

cpp

#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();
}

Security note: WiFi credentials and Google Apps Script deployment IDs are placeholders above (YOUR_WIFI_SSID, etc.) — replace them with your own before uploading, and avoid committing real credentials to any public repository or writeup.

Security note: WiFi credentials and Google Apps Script deployment IDs are placeholders above (YOUR_WIFI_SSID, etc.) — replace them with your own before uploading, and avoid committing real credentials to any public repository or writeup.

Enclosure: JUSTWAY

A wall-mounted attendance terminal that students interact with dozens of times a day needs to survive more handling than a typical prototype — repeated card taps, curious fingers, and the occasional bump. For the final build, the Arduino UNO R4 WiFi, RYRR30D module, OLED display, and all wiring were housed inside a JUSTWAY enclosure, turning the breadboard prototype into a genuinely classroom-ready device.

Why the Enclosure Choice Matters Here

This project has more going on than a typical single-sensor build — an OLED that needs to stay visible, an antenna that needs to stay clear, and daily handling from multiple students — so the enclosure layout had several specific requirements:

From Prototype to Production

image.png


One thing I particularly like about JUSTWAY is that it isn't limited to only 3D printing. The platform is designed to support the complete product development cycle—from rapid prototyping to low-volume manufacturing and eventually mass production.

Depending on your application, the platform offers several manufacturing services, including:

According to JUSTWAY, these services support customers ranging from individual makers and engineering students to startups and industrial manufacturers working in consumer electronics, robotics, automotive, aerospace, medical, and industrial automation.

Uploading Your CAD Design

The ordering process is refreshingly simple.

Instead of exchanging multiple emails for quotations, you simply upload your CAD model directly through the online quotation system.

The platform supports several commonly used engineering formats including:

making it compatible with popular CAD software such as Fusion 360, SolidWorks, Autodesk Inventor, Creo, FreeCAD, and Onshape.

Once uploaded, the model is automatically prepared for quotation, allowing you to continue configuring the manufacturing parameters immediately.

For projects containing multiple components, several files can also be uploaded together, making it convenient to manufacture complete assemblies rather than individual parts.

Configuring the Manufacturing Parameters

image.png


After uploading the model, the platform provides a detailed configuration panel where almost every manufacturing parameter can be customized before placing the order.

Material Selection

Depending on the manufacturing process, a wide range of materials is available.

For FDM 3D printing, commonly available materials include:

For CNC machining, users can choose from materials such as:

Each material offers different characteristics depending on whether the priority is strength, heat resistance, flexibility, surface finish, or dimensional accuracy.

SLA Resin Printing Options

For projects requiring extremely fine details or presentation-quality models, JUSTWAY also offers SLA resin printing with multiple specialized materials.

Available resin options include:

These materials are designed for different applications ranging from display models and transparent covers to engineering validation, electronic fixtures, medical prototypes, and functional mechanical components.

For example:

Having access to different resin formulations makes it much easier to choose the right balance between appearance, strength, and functionality.

Color and Finish Options

The platform also allows users to customize the appearance of printed parts.

Depending on the selected material, various colors and finishes are available, including standard colors, silk finishes, matte textures, translucent options, and specialty finishes.

This is particularly useful when manufacturing demonstration models, educational kits, exhibition prototypes, or consumer-facing products where aesthetics are just as important as functionality.

Instant Online Quotation

image.png

One feature I personally appreciate is the instant quotation system.

As different materials, quantities, manufacturing technologies, or finishing options are selected, the estimated manufacturing cost updates automatically.

The quotation page also provides useful information such as:

Being able to compare multiple material options within a few minutes makes it much easier to optimize both performance and budget before placing an order.

Engineering Support Throughout the Process

Beyond manufacturing, JUSTWAY also offers several features that simplify product development.

Their platform provides instant online quotations, design review before production, order management, production tracking, and shipping updates, making it easy to monitor the entire manufacturing process from a single dashboard.

For larger production runs, additional services such as dedicated engineering support, DFM (Design for Manufacturability) review, First Article Inspection (FAI), and quality inspection reports are also available to help ensure consistent production quality.

Final Thoughts

Whether you're building a one-off hobby project, a university engineering project, an open-source hardware design, or preparing a commercial product, custom manufactured parts can significantly improve both reliability and presentation.

Having used JUSTWAY for several of my own projects, I found the platform straightforward to use—from uploading CAD files and selecting materials to receiving an instant quotation and tracking production. Its combination of rapid prototyping, precision manufacturing, and scalable production services makes it a practical solution for makers, students, startups, and professional hardware developers looking to turn their ideas into finished products.

Testing and Calibration

Before wiring everything into the final enclosure, the RYRR30D was validated independently using a serial terminal (Docklight) at 115200 baud — issuing the AT+APPLE and AT+MODE=2 commands manually, confirming +OK responses, and testing both a phone tap (+APPLE=...) and RFID card taps (+ISO14443A=...) separately. This isolated reader/firmware issues from enclosure and wiring issues, and specifically surfaced the "phantom +ISO14443A after a phone tap" behavior early — which directly shaped the settle-state design in the final firmware.

It's also worth testing the Google Apps Script deployment URL directly in a browser (e.g., appending ?uid=TEST&name=Test&status=Present) before relying on the Arduino to reach it — this confirms the script and sheet permissions are correct independent of any networking code.

Possible Extensions

This system is deliberately simple to extend:

Conclusion

This project shows how a single certified NFC/RFID module can cover two very different authentication needs — a teacher's phone-based Apple Wallet pass and a classroom full of physical RFID cards — without needing separate hardware for each. The REYAX RYRR30D handles both protocols over one UART connection, the Arduino UNO R4 WiFi's built-in networking pushes every attendance event straight into a Google Sheet without any external gateway hardware, and a JUSTWAY enclosure turns the whole thing into a device that could realistically sit on a classroom wall and be used every single school day.