FluxBench.AIShared project · read-only

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.