HID R10 / R15 Desktop RFID Arduino Door Simulator (Project Complete)

This should be a straight copy/paste in the first post.

I see this all the time in Deviant Ollams videos and i wanted to try my hand at making it.

Parts

Case - HID R10 and RP15 Case with Arduino, Oled, and Voltage Stepdown by Hamspiced - Thingiverse

HID R10 Multi-class reader - Find these on Ebay for around 20-50$ used

Arduino Nano - $16.49 per 3 or $5.49 ea

USB-C Pigtail - $7.99 per 10 or $0.80 ea

Arduino Breakout Board - $8.79 per 3 or $2.93 ea

5v Buck Converter - $11.59 per 5 or $2.32 ea

i2c 2 Color OLED - $13.98 per 5 or $2.80 ea

Secure R10 to top of case with screw and nut
Secure arduino to breakout board and breakout board to bottom case. Secure Stepdown module to bottom case

Connect USB-C Power to Stepdown input, then jumper stepdown output to Arduino at USB in and Ground.

Wire up i2c screen on 5v of arduino and ground as well as the rest of the I2c Pins.

Wire HID R10/R15 to arduino. Secure with screws.

I want to change the code to more reflect how @DeviantOllam does his. With different modes of use (theres a card read mode, and then an access controller mode that makes it function like a door reader. However i want to actually make it a door reader with a mag striker and relay), however i am not THAT great at arduino code. if anyone is please reach out id love to bounce my ideas off of you. I wish his code was opensourced but i dont think he wants to release that IP.

I wanted to get this out to everyone because it really is a cool project and it didnt take too long to spin up. You can turn it wireless by adding a ESPKey to it and then just use a powerbank to power it off of the port.

If there is interest i will do a full write-up and pinout.

R15 to Stepdown Module To Arduino Pinout

HID Reader to Arduino Uno

Black  => Gnd
Green  => D2
White  => D3
Brown  => D5
Orange => D6
Yellow => A3

OLED to Arduino
VCC => 5v
GND => Ground
SCL => A5
SDA => A4

POWER IN TO STEPDOWN
BLACK => - IN
RED   => + IN

STEPDOWN to Arduino
- out  => GND
+ out => VIN

Current Arduino Code

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wiegand.h>
#include <EEPROM.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

#define WIEGAND_D0_PIN 2 // Green wire (Data 0)
#define WIEGAND_D1_PIN 3 // White wire (Data 1)
#define BEEPER_PIN A3
#define SIGNAL_PIN 7 // Door strike signal pin

#define MAX_CARDS 20
#define EEPROM_CARD_COUNT_ADDR 0
#define EEPROM_CARD_DATA_ADDR 4
#define CARD_SIZE sizeof(unsigned long)

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
WIEGAND wg;

bool cardDisplayed = false;
unsigned long lastCardData = 0;
uint8_t lastBitLength = 0;
uint8_t lastFacilityCode = 0;
uint16_t lastCardNumber = 0;

#define MODE_READ 1
#define MODE_DOOR 2
#define MODE_ADD 3
#define MODE_REMOVE 4
uint8_t currentMode = MODE_READ;

const unsigned long configCards[4] = {
  0x1E6DC032, // Read mode
  0x1E6D9861, // Door mode
  0x1E6DFCA0, // Add mode
  0x1E6DE636  // Remove mode
};

unsigned long authorizedCards[MAX_CARDS];
uint8_t cardCount = 0;

uint8_t countBits(uint32_t data) {
  uint8_t count = 0;
  while (data) {
    data >>= 1;
    count++;
  }
  return count;
}

void loadAuthorizedCards() {
  cardCount = EEPROM.read(EEPROM_CARD_COUNT_ADDR);
  if (cardCount > MAX_CARDS) {
    cardCount = 0;
    return;
  }
  for (uint8_t i = 0; i < cardCount; i++) {
    unsigned long cardData = 0;
    for (uint8_t j = 0; j < CARD_SIZE; j++) {
      cardData |= (unsigned long)EEPROM.read(EEPROM_CARD_DATA_ADDR + i * CARD_SIZE + j) << (8 * j);
    }
    authorizedCards[i] = cardData;
  }
}

void saveAuthorizedCards() {
  EEPROM.update(EEPROM_CARD_COUNT_ADDR, cardCount);
  for (uint8_t i = 0; i < cardCount; i++) {
    unsigned long cardData = authorizedCards[i];
    for (uint8_t j = 0; j < CARD_SIZE; j++) {
      EEPROM.update(EEPROM_CARD_DATA_ADDR + i * CARD_SIZE + j, (cardData >> (8 * j)) & 0xFF);
    }
  }
}

bool isAuthorized(unsigned long cardData) {
  for (uint8_t i = 0; i < cardCount; i++) {
    if (authorizedCards[i] == cardData) {
      return true;
    }
  }
  return false;
}

void addCard(unsigned long cardData) {
  if (cardCount < MAX_CARDS && !isAuthorized(cardData)) {
    authorizedCards[cardCount] = cardData;
    cardCount++;
    saveAuthorizedCards();
  }
}

void removeCard(unsigned long cardData) {
  for (uint8_t i = 0; i < cardCount; i++) {
    if (authorizedCards[i] == cardData) {
      for (uint8_t j = i; j < cardCount - 1; j++) {
        authorizedCards[j] = authorizedCards[j + 1];
      }
      cardCount--;
      saveAuthorizedCards();
      break;
    }
  }
}

void displayScanPrompt() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(12, 0);
  switch (currentMode) {
    case MODE_READ:   display.println("READ MODE"); break;
    case MODE_DOOR:   display.println("DOOR MODE"); break;
    case MODE_ADD:    display.println("ADD MODE"); break;
    case MODE_REMOVE: display.println("DEL MODE"); break;
  }
  display.setTextSize(1);
  display.setCursor(25, 25);
  display.println("Scan a card...");
  display.display();
  cardDisplayed = false;
}

void displayCardData(unsigned long cardData, uint8_t bitLength, uint8_t facilityCode, uint16_t cardNumber, const char* message = "") {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(12, 0);
  switch (currentMode) {
    case MODE_READ:   display.println("READ MODE"); break;
    case MODE_DOOR:   display.println("DOOR MODE"); break;
    case MODE_ADD:    display.println("ADD MODE"); break;
    case MODE_REMOVE: display.println("DEL MODE"); break;
  }
  display.setTextSize(1);
  if (currentMode == MODE_READ) {
    display.setCursor(0, 20);
    display.print("Hex: 0x");
    display.println(cardData, HEX);
    display.setCursor(0, 30);
    display.print("Raw: ");
    display.println(cardData);
    display.setCursor(0, 40);
    display.print("Bits: ");
    display.print(bitLength);
    display.println(" (approx)");
    display.setCursor(0, 50);
    display.print("FC: ");
    display.print(facilityCode);
    display.print(" ID: ");
    display.println(cardNumber);
  } else {
    display.setCursor(30, 20); // Center message
    display.println(message);
  }
  display.display();
  cardDisplayed = true;
}

void setup() {
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    while (1);
  }

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(25, 16);
  display.println("RFIDsim");
  display.setTextSize(1);
  display.setCursor(31, 40);
  display.println("v1.0 By Hamspiced and Peekaboo");
  display.display();
  delay(1000);

  loadAuthorizedCards();
  displayScanPrompt();

  wg.begin(WIEGAND_D0_PIN, WIEGAND_D1_PIN);

  pinMode(BEEPER_PIN, OUTPUT);
  digitalWrite(BEEPER_PIN, HIGH);
  pinMode(SIGNAL_PIN, OUTPUT);
  digitalWrite(SIGNAL_PIN, LOW);
}

void loop() {
  if (wg.available()) {
    unsigned long cardData = wg.getCode();

    // Check for configuration card
    for (uint8_t i = 0; i < 4; i++) {
      if (cardData == configCards[i]) {
        currentMode = i + 1;
        displayScanPrompt();
        digitalWrite(BEEPER_PIN, LOW);
        delay(100);
        digitalWrite(BEEPER_PIN, HIGH);
        return;
      }
    }

    // Process card data
    uint8_t bitLength = countBits(cardData);
    uint8_t facilityCode = (cardData >> 16) & 0xFF;
    uint16_t cardNumber = cardData & 0xFFFF;

    digitalWrite(BEEPER_PIN, LOW);
    delay(50);
    digitalWrite(BEEPER_PIN, HIGH);

    lastCardData = cardData;
    lastBitLength = bitLength;
    lastFacilityCode = facilityCode;
    lastCardNumber = cardNumber;

    // Handle mode-specific actions
    switch (currentMode) {
      case MODE_READ:
        displayCardData(cardData, bitLength, facilityCode, cardNumber);
        break;
      case MODE_DOOR:
        if (isAuthorized(cardData)) {
          digitalWrite(SIGNAL_PIN, HIGH);
          displayCardData(cardData, bitLength, facilityCode, cardNumber, "Authorized");
          delay(5000); // Show for 5 seconds
          digitalWrite(SIGNAL_PIN, LOW);
          displayScanPrompt();
        } else {
          displayCardData(cardData, bitLength, facilityCode, cardNumber, "Access Denied");
          delay(5000); // Show for 5 seconds
          displayScanPrompt();
        }
        break;
      case MODE_ADD:
        addCard(cardData);
        displayCardData(cardData, bitLength, facilityCode, cardNumber, "Card Added");
        break;
      case MODE_REMOVE:
        removeCard(cardData);
        displayCardData(cardData, bitLength, facilityCode, cardNumber, "Card Removed");
        break;
    }
  } else if (cardDisplayed) {
    displayCardData(lastCardData, lastBitLength, lastFacilityCode, lastCardNumber);
  } else {
    displayScanPrompt();
  }
}
8 Likes

What ideas do you have in-mind for the code?

1 Like

I have some basic code from another git that allows you to make the reader operate in different modes.

But I want to adapt it to be how Deviant has his. His I believe functions like this:

One for just basic card reading where it decrypts the wiegand data and displays it by bit length, facility, and ID.

the other mode is door access control. Displays Authorized, unotherized and the card key info.

the third mode is to add cards. So you scan a card, it puts it into add card mode. Then you can scan 2-3 more cards and it adds it to the authorized card list.

the fourth mode is just like the 3rd but it removes cards.

Modes are all selected by scanning different configuration cards.

1 Like

And you want to integrate that code or duplicate the functionality?

1 Like

You must have replied before my edit went through.

The sample I has shows that it’s possible to do different modes with card scans, but I want to do it as outlined which I believe is how Deviant set it up.

I can do it, but it will take time for me to learn. Which I don’t mind, I just have other things I’m working on in tandem

2 Likes

Ah gotcha, makes sense. I have a good bit of experience coding arduinos and such. Unfortunately I don’t have much free time currently so I can’t lead dev efforts, but I’m more than happy to help how I can

3 Likes

That’s all I need, someone to check my code and tell me why I’m an idiot rather than just confirming what my wife reinforces

6 Likes

You should take a look at GitHub - evildaemond/doorsim: An Open-Source Door Simulator for RFID/PACS Training they have an interesting firmware with a similar project.

2 Likes

I made a similar setup but for home automation using Home Assistant.

You could use do the same thing with your reader and have a desktop reader that integrates with Home Assistant

2 Likes

I love that idea. Is the code for an esp32 easily moved to an Arduino?

Nevermind I just read the git.

Yeah you could easily add a espkey in line and use both. I know nothing about programming esp32

2 Likes

Esp32’s support arduino. You need to add them as a board in the arduino ide but then the code is pretty much the same

3 Likes

Alright I may look into this. I know nothing about them other than using wiled and espkey

3 Likes

I use them frequently, I’m happy to answer any questions you run into

2 Likes

@Jammyjellyfish

Havent dipped into the ESP32 yet. because i dont have a plethora of them currently.

But can you review what i currently have. I added some features like pulling out Card # and Facility code from the Wiegand data, and i added the mode cases for adding a card, removing a card, and setup a pin to send a signal for a relay for a door access.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wiegand.h>

#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);
WIEGAND wg;

#define MAX_CARDS 20  
uint32_t authorizedCards[MAX_CARDS];
uint8_t cardCount = 0;

const uint32_t modeCards[4] = {1, 2, 3, 4}; 
uint8_t currentMode = 1;

uint8_t countBits(uint32_t data) {
  uint8_t count = 0;
  while (data) {
    data >>= 1;    // Right shift by 1 to move through each bit
    count++;       // Count each shift as a bit
  }
  return count;
}

uint8_t getFacilityCode(uint32_t data) {
  return (data >> 16) & 0xFF;  // Extract the top 8 bits as facility code
}

uint16_t getCardNumber(uint32_t data) {
  return data & 0xFFFF;  // Extract the lower 16 bits as the card number
}
#define SIGNAL_PIN 13    
#define BEEPER_PIN 12     

void setup() {
  delay(500);
  Serial.begin(9600);

  // Initialize OLED display
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS, OLED_RESET)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);  
  }
  display.clearDisplay();
  display.display();  // Ensure display is clear
  display.setTextSize(2);
  display.setTextColor(WHITE);

  wg.begin();

  pinMode(SIGNAL_PIN, OUTPUT);
  digitalWrite(SIGNAL_PIN, LOW);

  pinMode(BEEPER_PIN, OUTPUT);
  digitalWrite(BEEPER_PIN, LOW);  // Ensure beeper is off initially

  // Display "Read Mode" directly on startup
  display.clearDisplay();
  display.setCursor(13, 0);
  display.print("Read Mode");
  display.display();
}

void loop() {
  if (wg.available()) {
    uint32_t cardData = wg.getCode();  
    uint8_t bitLength = countBits(cardData);  // Manually calculate bit length
    uint8_t facilityCode = getFacilityCode(cardData);  // Get facility code
    uint16_t cardNumber = getCardNumber(cardData);  // Get card number

    // Debugging: Print card data, bit length, and facility code to Serial Monitor
    Serial.print("Card Data: ");
    Serial.println(cardData);
    Serial.print("Bit Length: ");
    Serial.println(bitLength);
    Serial.print("Facility Code: ");
    Serial.println(facilityCode);
    Serial.print("Card Number: ");
    Serial.println(cardNumber);

    // Beep to indicate card read
    beep();

    // Check if the scanned card is a mode-changing card
    for (uint8_t i = 0; i < 4; i++) {
      if (cardData == modeCards[i]) {
        currentMode = i + 1;
        displayMode();
        return;
      }
    }

    // Perform actions based on the current mode
    switch (currentMode) {
      case 1:
          display.clearDisplay();
          display.setCursor(13, 0);
          display.setTextSize(2);
          display.print("Read Mode");

          display.setTextSize(1);
          display.setCursor(0, 22);
          display.print("Card #: ");
          display.print(cardNumber);  // Show only the card number part

          display.setCursor(0, 32);
          display.print("FC: ");
          display.print(facilityCode);

          display.setCursor(0, 42);
          display.print("Bit Len: ");
          display.print(bitLength);

          display.setCursor(0, 52);
          display.print("Raw Data: ");
          display.print(cardData);

          display.display();
          break;

      case 2:
        if (isAuthorized(cardData)) {
          digitalWrite(SIGNAL_PIN, HIGH);  
          delay(500);                      
          digitalWrite(SIGNAL_PIN, LOW);   
          displayData("Door Mode", "Authorized", "", "");
        } else {
          displayData("Door Mode", "Denied", "", "");
        }
        break;

      case 3:
        addCard(cardData);
        displayData("Add Card Mode", "Card Added", "Card Data: " + String(cardData), "");
        break;

      case 4:
        removeCard(cardData);
        displayData("Remove Card Mode", "Card Removed", "Card Data: " + String(cardData), "");
        break;
    }
  }
}

void beep() {
  digitalWrite(BEEPER_PIN, HIGH);  
  delay(50);                     
  digitalWrite(BEEPER_PIN, LOW);  
}

void displayMode() {
  String modeText = "";
  switch (currentMode) {
    case 1: modeText = "Read Mode"; break;
    case 2: modeText = "Door Mode"; break;
    case 3: modeText = "Add Card Mode"; break;
    case 4: modeText = "Remove Card Mode"; break;
  }
  displayData(modeText, "", "", "");
}

void displayData(String mode, String line1, String line2, String line3) {
  display.clearDisplay();   // Clear display buffer
  display.setTextSize(2);

  display.setCursor(13, 0);
  display.setTextColor(WHITE);
  display.print(mode);

  display.setTextSize(1);
  display.setCursor(0, 20);
  display.print(line1);
  display.setCursor(0, 30);
  display.print(line2);
  display.setCursor(0, 40);
  display.print(line3);

  display.display();        // Refresh display to show updated content
}

bool isAuthorized(uint32_t data) {
  for (uint8_t i = 0; i < cardCount; i++) {
    if (authorizedCards[i] == data) {
      return true;
    }
  }
  return false;
}

void addCard(uint32_t data) {
  if (cardCount < MAX_CARDS && !isAuthorized(data)) {
    authorizedCards[cardCount] = data;
    cardCount++;
  }
}

void removeCard(uint32_t data) {
  for (uint8_t i = 0; i < cardCount; i++) {
    if (authorizedCards[i] == data) {
      for (uint8_t j = i; j < cardCount - 1; j++) {
        authorizedCards[j] = authorizedCards[j + 1];
      }
      cardCount--;
      break;
    }
  }
}
2 Likes

i also added the RP15 case design so you have 2 options for cases.

Rp15 Bottom.stl (359.1 KB)
Rp15 Top.stl (135.4 KB)
R10 Case bottom.stl (347.2 KB)
R10 Case Top.stl (139.9 KB)

i ended up buying a lot of RP15’s because it was cheaper than buying them individually, so if someone wants to build this i have a few i can sell. 20$ each

3 Likes

Looks like a good start!

Some quick code style comments:

  • You don’t need the delay on line 41
  • When organizing code, I personally like all my defines up right under the includes lines
  • modeCards could be an enum, it will help a lot with code readability (or at least a comment with what the modes mean :grin: )
  • not a big deal in this case, but the 50ms delay in beep() is blocking, that’s pretty long if the code needs to do other things

I’d recommend taking a look at the eeprom library next, it will allow you to save the card arrays to non-volatile memory, so you don’t lose them when you turn the system off

Run into any pain points while getting it this far?

4 Likes

Awesome! How’s the scan performance? I may get one or two from you =)

2 Likes

Its needed for the OLED. you need to add a delay for the screen to process the information. otherwise it wont turn on at start, but will turn on at the first card read.

Im not a coder, so i assumed i seem to have just added them as needed. Be happy i comment to the best of my ability. Ill move them.

I have no idea what this means or entails.

I am having issues with the beep. It may be the reader, but the reader itself doesnt beep when scanned like the R10. running the beep circuit it has a brain melting low beep hum non-stop even with the code setting the pin to low. so currently beep isnt functioning correctly.

I would really like to do this but my time to work on this project is starting to dwindle as my free-time has to switch focus to my FTJ. if you want to send me some documentation regarding on how to switch this ill look at it as my freetime frees up again.

The biggest pain-point i had was in regards to displaying the wiegand data correctly. for some reason i had a hell of a time counting the bit-length.

Actually really good. I like the R10 more just because of it’s form-factor but it has great performance. however i cannot get it to read either my Xmagic or my Xsiid. it wont even light the Xsiid but the R10 did just fine.

4 Likes

You may want to look into adding a pullup or pulldown to on the beeper pin. Most wiegand readers I’ve dealt with grounding the beep pin made it beep so if it’s kind of floating it can cause these issues.

I might spin up a copy of the hardware here with your code for testing :slight_smile:

3 Likes

This explains a lot. I’ll add my in line.

edit: did not work for me, suspecting i have a bad internal beeper on this unit as it works perfectly fine on the R10

Edit2: Placing a Repeater sticker on the RP15 in this location allows it to read Xsiid’s perfectly fine.

5 Likes