FluxBench.AIShared project · read-only

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.