RFID Coffee Card
ESP32 DevKit (WROOM-32)·Arduino C++·Updated Jul 27, 2026·by FluxBench TeamIncludes Lab + Schematic
A stored-value loyalty card where the balance lives on the card itself. The sketch authenticates MIFARE Classic block 4 through the MFRC522 reader, reads the stored balance, charges $0.75 per coffee (green LED) and writes the new balance back to the card. Fresh cards are initialized with $3.00 starter credit on first tap; when the credit runs out the red LED flashes DECLINED, and pressing the bench button arms a $1.00 top-up for the next tap. Card A and card B on the reader panel each keep their own independent balance for the whole run.
sketch.ino
// RFID Coffee Card - a stored-value card, like a real cafe loyalty card.
// The balance lives ON the card itself: MIFARE Classic cards carry 1 KB of
// storage split into 16-byte blocks, and this sketch reads and writes
// block 4 through the MFRC522 reader (authenticate -> read -> write).
// Every tap buys a $0.75 coffee. Press the button to arm a $1.00 top-up
// for the next tap. Fresh cards get $3.00 starter credit on first tap.
// Tap card A and card B on the reader panel - each keeps its own balance.
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SS_PIN 5
#define RST_PIN 27
#define LED_OK 4 // green: coffee served
#define LED_NO 2 // red: declined
#define BTN_TOPUP 14 // arm a top-up for the next tap
#define BLOCK 4 // card data block that stores the balance
#define PRICE 75 // coffee price in cents
#define TOPUP 100 // top-up amount in cents
MFRC522 rfid(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key keyA;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
int cups = 0;
bool topupArmed = false;
int lastBtn = 1;
void showScreen(int balance, const char* msg) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print("COFFEE CARD cups:");
display.print(cups);
display.setTextSize(2);
display.setCursor(0, 14);
if (balance >= 0) {
char buf[12];
sprintf(buf, "$%d.%02d", balance / 100, balance % 100);
display.print(buf);
} else {
display.print("TAP CARD");
}
display.setTextSize(1);
display.setCursor(0, 38);
display.print(msg);
display.setCursor(0, 54);
if (topupArmed) {
display.print("TOP-UP ARMED +$1.00");
} else {
display.print("Button = arm top-up");
}
display.display();
}
void setup() {
Serial.begin(115200);
pinMode(LED_OK, OUTPUT);
pinMode(LED_NO, OUTPUT);
pinMode(BTN_TOPUP, INPUT_PULLUP);
SPI.begin();
rfid.PCD_Init();
for (byte i = 0; i < 6; i++) keyA.keyByte[i] = 0xFF; // factory default key A
Wire.begin();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
showScreen(-1, "Tap to buy a coffee");
Serial.println("Coffee Card ready - every tap buys a $0.75 coffee");
}
void loop() {
int btn = digitalRead(BTN_TOPUP);
if (btn == 0 && lastBtn == 1) {
topupArmed = true;
Serial.println("Top-up armed: the next card tapped gets +$1.00");
showScreen(-1, "Tap card to top up");
}
lastBtn = btn;
if (!rfid.PICC_IsNewCardPresent()) {
delay(50);
return;
}
if (!rfid.PICC_ReadCardSerial()) {
delay(50);
return;
}
byte status = rfid.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, BLOCK, &keyA, &(rfid.uid));
if (status != MFRC522::STATUS_OK) {
Serial.println("Auth failed - card left the field too early");
delay(50);
return;
}
byte buffer[18];
byte size = 18;
status = rfid.MIFARE_Read(BLOCK, buffer, &size);
if (status != MFRC522::STATUS_OK) {
Serial.println("Block read failed");
delay(50);
return;
}
int balance = 0;
bool fresh = true;
if (buffer[0] == 0xC0 && buffer[1] == 0xFE) {
fresh = false;
balance = (buffer[2] << 8) | buffer[3];
}
const char* msg = "";
if (fresh) {
balance = 300;
msg = "NEW CARD +$3.00";
Serial.print("New card - loading $3.00 starter credit");
} else if (topupArmed) {
balance = balance + TOPUP;
topupArmed = false;
msg = "TOPPED UP +$1.00";
Serial.print("TOP-UP +$1.00 accepted");
} else if (balance >= PRICE) {
balance = balance - PRICE;
cups = cups + 1;
digitalWrite(LED_OK, HIGH);
msg = "COFFEE SERVED -$0.75";
Serial.print("COFFEE SERVED -$0.75");
} else {
digitalWrite(LED_NO, HIGH);
msg = "DECLINED - top up!";
Serial.print("DECLINED - insufficient credit, press the top-up button");
}
byte block[16];
for (int i = 0; i < 16; i++) block[i] = 0;
block[0] = 0xC0;
block[1] = 0xFE;
block[2] = (balance >> 8) & 0xFF;
block[3] = balance & 0xFF;
status = rfid.MIFARE_Write(BLOCK, block, 16);
if (status != MFRC522::STATUS_OK) {
Serial.print(" (write failed)");
}
Serial.print(" balance now $");
Serial.print(balance / 100);
Serial.print(".");
if (balance % 100 < 10) Serial.print("0");
Serial.println(balance % 100);
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
showScreen(balance, msg);
delay(700);
digitalWrite(LED_OK, LOW);
digitalWrite(LED_NO, LOW);
}Built with FluxBench.AI — the AI workbench for embedded projects.