Arcade Token Card
ESP32 DevKit (WROOM-32)·Arduino C++·Updated Jul 27, 2026·by FluxBench TeamIncludes Lab + Schematic
MIFARE value blocks done properly. Where the Coffee Card demo stores its balance as raw bytes, this arcade token dispenser uses the real MIFARE Classic value-block machinery: tokens live in value block 6 (value + complement + copy, self-verifying), each play runs an atomic MIFARE_Decrement + MIFARE_Transfer, the bench button arms a +5 token MIFARE_Increment, fresh cards are formatted with MIFARE_SetValue, and after every transaction the balance is mirrored into backup block 5 with the classic MIFARE_Restore → MIFARE_Transfer idiom that real turnstiles use to survive power loss mid-write. Card A and card B each keep their own token balance.
sketch.ino
// Arcade Token Card - MIFARE value blocks done the RIGHT way.
// Unlike a plain data block, a MIFARE Classic VALUE BLOCK stores a signed
// 32-bit number three times (value, ~value, value) plus address bytes, so
// the card itself can verify integrity - and the reader gets atomic
// Increment / Decrement / Transfer commands with a backup-block idiom that
// survives power loss mid-write. This sketch keeps arcade tokens in value
// block 6 with a backup copy in block 5 of the same sector.
//
// Every tap plays a game: MIFARE_Decrement(6, 1) -> MIFARE_Transfer(6).
// Press the button to arm a +5 token top-up (MIFARE_Increment) for the
// next tap. Fresh cards get 10 starter tokens via MIFARE_SetValue. After
// each transaction the value is mirrored to the backup block with
// MIFARE_Restore(6) -> MIFARE_Transfer(5), exactly like real turnstile
// and vending systems do. Tap card A and card B - separate balances.
#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: game credited
#define LED_NO 2 // red: out of tokens
#define BTN_TOPUP 14 // arm a +5 token top-up for the next tap
#define VALUE_BLOCK 6 // sector 1 value block holding the token count
#define BACKUP_BLOCK 5 // backup copy in the same sector
#define STARTER 10 // tokens loaded onto a fresh card
#define TOPUP 5 // tokens added per top-up
MFRC522 rfid(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key keyA;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
int plays = 0;
bool topupArmed = false;
int lastBtn = 1;
void showScreen(long tokens, const char* msg) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print("ARCADE TOKENS plays:");
display.print(plays);
display.setTextSize(2);
display.setCursor(0, 14);
if (tokens >= 0) {
display.print(tokens);
display.print(" TKN");
} 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 +5 TKN");
} 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 play (1 token)");
Serial.println("Arcade ready - value block 6, backup block 5, 1 token per play");
}
void loop() {
int btn = digitalRead(BTN_TOPUP);
if (btn == 0 && lastBtn == 1) {
topupArmed = true;
Serial.println("Top-up armed: next card tapped gets +5 tokens");
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, VALUE_BLOCK, &keyA, &(rfid.uid));
if (status != MFRC522::STATUS_OK) {
Serial.println("Auth failed - card left the field too early");
delay(50);
return;
}
long tokens = 0;
status = rfid.MIFARE_GetValue(VALUE_BLOCK, &tokens);
const char* msg = "";
if (status != MFRC522::STATUS_OK) {
// Fresh card: block 6 is not in value format yet. Format both blocks.
rfid.MIFARE_SetValue(VALUE_BLOCK, STARTER);
rfid.MIFARE_SetValue(BACKUP_BLOCK, STARTER);
tokens = STARTER;
msg = "NEW CARD +10 TKN";
Serial.print("New card - formatted value block with 10 starter tokens");
} else if (topupArmed) {
rfid.MIFARE_Increment(VALUE_BLOCK, TOPUP);
rfid.MIFARE_Transfer(VALUE_BLOCK);
topupArmed = false;
msg = "TOP-UP +5 TKN";
Serial.print("TOP-UP +5 tokens accepted");
} else if (tokens >= 1) {
rfid.MIFARE_Decrement(VALUE_BLOCK, 1);
rfid.MIFARE_Transfer(VALUE_BLOCK);
plays = plays + 1;
digitalWrite(LED_OK, HIGH);
msg = "PLAY! -1 TKN";
Serial.print("GAME ON -1 token");
} else {
digitalWrite(LED_NO, HIGH);
msg = "EMPTY - top up!";
Serial.print("DECLINED - no tokens left, press the top-up button");
}
// Mirror the live value into the backup block (Restore loads block 6 into
// the transfer buffer, Transfer writes the buffer into block 5).
rfid.MIFARE_Restore(VALUE_BLOCK);
rfid.MIFARE_Transfer(BACKUP_BLOCK);
rfid.MIFARE_GetValue(VALUE_BLOCK, &tokens);
Serial.print(" tokens now ");
Serial.println(tokens);
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
showScreen(tokens, msg);
delay(700);
digitalWrite(LED_OK, LOW);
digitalWrite(LED_NO, LOW);
}Built with FluxBench.AI — the AI workbench for embedded projects.