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
- On power-up, the OLED shows a "Welcome To School" splash, connects to WiFi, and settles into a "Waiting For Teacher" idle state.
- The teacher taps their iPhone (carrying an authorized Apple Wallet pass) near the RYRR30D.
- The system verifies the pass matches the trusted teacher credential, shows a tick animation, then displays "Remove Phone" for a short settle period — this exists specifically to let the reader's leftover signals from that tap clear out (more on this below).
- The display then switches to "Tap Student Cards Now" — the attendance session is live.
Each student taps their RFID card:
- Valid, registered card → tick animation, marked "Present", logged to Google Sheets with name + UID + timestamp.
- Unregistered card → cross animation, logged as "Invalid".
- Each student taps their RFID card:Valid, registered card → tick animation, marked "Present", logged to Google Sheets with name + UID + timestamp.Unregistered card → cross animation, logged as "Invalid".
- When the teacher taps their iPhone again, the session ends: the OLED shows a "DONE" screen (inverted, full white background with black text), then returns to "Waiting For Teacher" for the next class period.
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:
- Apple Wallet VAS pass decoding for the teacher's phone-based authentication — no student-facing app, no PIN, just a tap.
- Standard ISO14443A RFID card reading for the students — ordinary, cheap RFID cards work directly, no special "smart" cards needed.
- All of this comes over a single UART interface (115200 baud, 8N1) using simple AT commands, so one module and one serial connection covers both authentication paths.
- Onboard LED and buzzer feedback gave an extra, code-independent confirmation signal during development and testing.
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:
- Product Datasheet:reyax.com/upload/products_download/download_file/RYRR30D.pdf — full pinout, electrical characteristics, and protocol support (ISO14443A/B, ISO15693, FeliCa, Apple VAS, Google Smart Tap)
- AT Command Guide & Application Notes: search "REYAX RYRR30D Application Notes" — covers the full pass-issuing workflow, from generating an Apple Merchant Key with OpenSSL to setting it on the module over UART
- REYAX official site:reyax.com — product listings and other REYAX RF/NFC modules
- Support:sales@reyax.com for datasheets, custom pass integration questions, or bulk ordering
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
- When the teacher's tap is confirmed, the system doesn't jump straight into accepting student cards. It enters a SETTLING state for 3.5 seconds, during which the OLED explicitly instructs "Remove Phone" and any+ISO14443A= events are unconditionally discarded — no exceptions, no timing calculations, just a flat ignore window.
- Only after the settle period ends does the system move to IN_SESSION and start genuinely accepting student card taps.
- A debounce timer (6 seconds) on the teacher's Apple event also prevents one long phone tap from being read as two separate start/stop triggers.
- A hold window briefly delays processing of any student card read too, discarding it if an +APPLE= event arrives immediately after (confirming it was actually a teacher-phone phantom rather than a genuine card).
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:
- Antenna placement over durability: The RYRR30D's antenna area was positioned flush against a cutout with no metal shielding nearby, since NFC read range is short-range to begin with and easily blocked by metal. Because this reader gets tapped by every student every day, the cutout also needed to be reinforced enough not to wear down or crack under repeated contact.
- OLED visibility and angle: Unlike a purely LED/buzzer-fed system, this build relies on readable on-screen text ("Tap Student Cards Now, " tick/cross animations, "DONE") that students need to see clearly, often while queued up in a line. The OLED was mounted at a front-facing angle with a clear acrylic or open window in the JUSTWAY case, positioned at a height and angle readable by students of varying heights.
- Separation of teacher and student interaction zones: Since the same antenna reads both the teacher's phone and student cards, the enclosure design labeled or visually distinguished the "tap zone" clearly, so day-to-day use doesn't create confusion about where to tap — especially important since a student's stray card tap during the teacher's settle window is deliberately ignored by the firmware and shouldn't happen due to user confusion in the first place.
- WiFi antenna clearance: With the UNO R4 WiFi needing a live connection to log attendance to Google Sheets, the enclosure avoided fully wrapping the board in metal or dense material that could degrade WiFi signal strength, particularly important if the unit is mounted near a metal door frame or filing cabinet, both common in classrooms.
- Cable management for multiple peripherals: This build has more wiring than a simple single-sensor project — RYRR30D UART lines (with the voltage divider), OLED I2C lines (SDA/SCL), and power distribution all needed to be routed cleanly inside a relatively compact case without crosstalk or accidental shorts, particularly around the resistor-based voltage divider circuit.
- Mountability at classroom height: JUSTWAY enclosures support wall/panel mounting, which allowed the finished unit to be fixed near the classroom door at a consistent, accessible height — turning what could have been a "device on the teacher's desk" into a proper fixed classroom terminal.
From Prototype to Production
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:
- 3D Printing for rapid prototyping, concept models, custom enclosures, and functional mechanical parts.
- CNC Machining for precision components manufactured from aluminum, stainless steel, brass, titanium, copper, and engineering plastics.
- Sheet Metal Fabrication for industrial enclosures, mounting brackets, control panels, and structural assemblies.
- Injection Molding for producing large quantities of plastic parts with consistent quality.
- Silicone Vacuum Casting for producing low-volume production-quality parts without investing in expensive tooling.
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:
- STL
- STEP / STP
- OBJ
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
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:
- PLA
- ABS
- PETG
- Nylon
- TPU
- Polycarbonate (PC)
- ASA
- Carbon Fiber Reinforced Materials
- Engineering-grade Thermoplastics
For CNC machining, users can choose from materials such as:
- Aluminum 6061
- Stainless Steel 304
- Brass
- Copper
- Titanium
- Engineering Plastics
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:
- Standard White Resin
- Standard Black Resin
- Transparent Resin (UTR-8100)
- Translucent Resin
- High-Temperature Resin
- Flexible Resin
- Engineering Resin
- Tough Resin
- ESD-Safe Resin
- Dental and Professional Resins
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:
- Transparent Resin is excellent for display windows, LED covers, and visual prototypes.
- Engineering Resins provide higher strength and dimensional stability for functional parts.
- Flexible Resins are suitable for rubber-like components.
- High-Temperature Resins perform better in applications exposed to elevated temperatures.
- ESD Resins are useful when manufacturing fixtures for handling sensitive electronic assemblies.
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
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:
- Manufacturing Cost
- Shipping Cost
- Estimated Delivery Time
- Shipping Method
- Total Price
- Part Weight
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:
- Multiple teacher passes — support co-teachers or substitute teachers with their own Apple Wallet credentials.
- Absentee summary — compare the day's logged UIDs against the full roster in Google Sheets to automatically flag absent students.
- Google Wallet support — the RYRR30D also supports Google Smart Tap (AT+GOOGLE=...), so Android-based teacher devices could be added alongside Apple Wallet.
- Per-period sessions — extend the state machine to tag each session with a period/subject, useful for schools tracking attendance per class period rather than per day.
- Local buzzer/LED feedback — in addition to the OLED, a physical LED or buzzer could reinforce valid/invalid taps for accessibility.
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.
Rohan Barnwal