Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/*
HID RFID Door Simulator (Arduino Nano)
- Modes: READ, DOOR (relay), ADD, REMOVE
- First-boot admin enrollment stored in EEPROM
- Authorized card list stored in EEPROM
- OLED status prompts, Wiegand reader, beeper, door strike signal
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Expand All @@ -16,15 +23,23 @@

#define MAX_CARDS 20
#define EEPROM_CARD_COUNT_ADDR 0
// EEPROM layout summary:
// [0] : cardCount (1 byte)
// [4..] : authorized cards table (MAX_CARDS * 4 bytes)
// [EEPROM_ADMIN_UID] : 4 bytes admin UID
// [EEPROM_ADMIN_FLAG]: 1 byte 0xA5 when admin enrolled
#define EEPROM_CARD_DATA_ADDR 4
#define CARD_SIZE sizeof(unsigned long)

#define ADMIN_UID_DEFAULT 253411519UL
#define ADMIN_UID_DEFAULT 1041238720UL
#define EEPROM_ADMIN_UID_ADDR (EEPROM_CARD_DATA_ADDR + (MAX_CARDS * CARD_SIZE))

// Set to 0 to silence all Serial output
#define ENABLE_SERIAL 0

#define EEPROM_ADMIN_FLAG_ADDR (EEPROM_ADMIN_UID_ADDR + 4)
#define ENROLLED_FLAG_VALUE 0xA5

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

Expand All @@ -43,6 +58,7 @@ uint8_t currentMode = MODE_READ;

unsigned long adminUID = ADMIN_UID_DEFAULT;

// Special Wiegand values used to directly select a mode
const unsigned long configCards[4] = {
0x1E6DC032, // Read mode
0x1E6D9861, // Door mode
Expand All @@ -53,6 +69,7 @@ const unsigned long configCards[4] = {
unsigned long authorizedCards[MAX_CARDS];
uint8_t cardCount = 0;

// Returns an approximate bit length for the provided value
uint8_t countBits(uint32_t data) {
uint8_t count = 0;
while (data) {
Expand Down Expand Up @@ -87,6 +104,7 @@ void saveAuthorizedCards() {
}
}

// True if cardData exists in authorizedCards
bool isAuthorized(unsigned long cardData) {
for (uint8_t i = 0; i < cardCount; i++) {
if (authorizedCards[i] == cardData) {
Expand All @@ -96,6 +114,7 @@ bool isAuthorized(unsigned long cardData) {
return false;
}

// Append card to list if not duplicate and space remains
void addCard(unsigned long cardData) {
if (cardCount < MAX_CARDS && !isAuthorized(cardData)) {
authorizedCards[cardCount] = cardData;
Expand All @@ -104,6 +123,7 @@ void addCard(unsigned long cardData) {
}
}

// Remove first matching entry from list and compact
void removeCard(unsigned long cardData) {
for (uint8_t i = 0; i < cardCount; i++) {
if (authorizedCards[i] == cardData) {
Expand All @@ -127,21 +147,74 @@ const char* modeName(uint8_t mode) {
}
}

// Enrollment banner to capture admin UID on first boot
void displayEnrollPrompt() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("ADMIN ENROLL");
display.setCursor(0, 20);
display.println("Scan Admin Card...");
display.display();
cardDisplayed = false;
promptDisplayed = true;
}

// If no admin is enrolled, block until a card is scanned and save as admin
void enrollAdminIfNeeded() {
if (adminUID != 0UL) return;
displayEnrollPrompt();
while (adminUID == 0UL) {
if (wg.available()) {
unsigned long cardData = wg.getCode();
adminUID = cardData;
saveAdminUID(adminUID);
digitalWrite(BEEPER_PIN, LOW);
delay(200);
digitalWrite(BEEPER_PIN, HIGH);
display.clearDisplay();
display.setTextSize(2);
display.setCursor(8, 0);
display.println("ADMIN");
display.setTextSize(1);
display.setCursor(0, 24);
display.println("Admin Card Saved");
display.setCursor(0, 40);
display.print("UID: 0x");
display.println(adminUID, HEX);
display.display();
delay(1500);
promptDisplayed = false;
break;
}
}
}

unsigned long loadAdminUID() {
unsigned long uid = 0;
for (uint8_t i = 0; i < 4; i++) {
uid |= (unsigned long)EEPROM.read(EEPROM_ADMIN_UID_ADDR + i) << (8 * i);
}
if (uid == 0xFFFFFFFFUL || uid == 0UL) {
return ADMIN_UID_DEFAULT;
uint8_t flag = EEPROM.read(EEPROM_ADMIN_FLAG_ADDR);
if (flag == ENROLLED_FLAG_VALUE) {
if (uid != 0UL && uid != 0xFFFFFFFFUL) {
return uid;
}
return 0UL;
}
// Backward compatibility: if UID exists but flag missing, set flag
if (uid != 0UL && uid != 0xFFFFFFFFUL) {
EEPROM.update(EEPROM_ADMIN_FLAG_ADDR, ENROLLED_FLAG_VALUE);
return uid;
}
return uid;
return 0UL;
}

void saveAdminUID(unsigned long uid) {
for (uint8_t i = 0; i < 4; i++) {
EEPROM.update(EEPROM_ADMIN_UID_ADDR + i, (uid >> (8 * i)) & 0xFF);
}
EEPROM.update(EEPROM_ADMIN_FLAG_ADDR, ENROLLED_FLAG_VALUE);
}

void appendLogEntry(unsigned long raw, uint8_t fc, uint16_t id) {
Expand All @@ -156,6 +229,7 @@ void appendLogEntry(unsigned long raw, uint8_t fc, uint16_t id) {
#endif
}

// Render the idle screen and await next card
void displayScanPrompt() {
display.clearDisplay();
display.setTextSize(2);
Expand All @@ -174,6 +248,7 @@ void displayScanPrompt() {
promptDisplayed = true;
}

// Render a scan result; in READ mode show details, otherwise show a status message
void displayCardData(unsigned long cardData, uint8_t bitLength, uint8_t facilityCode, uint16_t cardNumber, const char* message = "") {
display.clearDisplay();
display.setTextSize(2);
Expand Down Expand Up @@ -204,6 +279,7 @@ void displayCardData(unsigned long cardData, uint8_t bitLength, uint8_t facility
promptDisplayed = false;
}

// Initialize hardware, restore state, and enroll admin if needed
void setup() {
#if ENABLE_SERIAL
Serial.begin(115200);
Expand Down Expand Up @@ -236,16 +312,20 @@ void setup() {
Serial.print(" BEEPER:"); Serial.print(BEEPER_PIN);
Serial.print(" SIGNAL:"); Serial.println(SIGNAL_PIN);
#endif
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);

// Prompt for admin enrollment if needed
enrollAdminIfNeeded();

displayScanPrompt();
}

// Core event loop: mode switching and scan handling
void loop() {
if (wg.available()) {
unsigned long cardData = wg.getCode();
Expand Down
74 changes: 74 additions & 0 deletions Arduino nano Code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Arduino Nano HID Door Simulator

Minimal offline door access controller for HID Wiegand readers using an Arduino Nano and SSD1306 OLED.

- Modes: READ, DOOR (relay output), ADD, REMOVE
- First-boot admin enrollment (prompt to scan an Admin card; stored in EEPROM)
- Authorized card list stored in EEPROM
- Simple OLED UI and beeper feedback; no WiFi/web/HA on Nano

## Hardware

- Reader (Wiegand):
- D0 → Arduino `D2`
- D1 → Arduino `D3`
- Beeper → Arduino `A3`
- Door strike/relay signal: Arduino `D7`
- OLED (SSD1306 I2C):
- SDA → `A4`
- SCL → `A5`
- Power: 5V via step-down; the Arduino USB-C pigtail is power-only and does not pass data

## First Boot: Admin Enrollment

On first boot (or after EEPROM reset) the OLED shows:
- "ADMIN ENROLL"
- "Scan Admin Card..."

Scan the badge you want to act as the Admin. It is saved to EEPROM and used to cycle modes when scanned later. This prompt will not reappear unless you clear EEPROM.

## Using the Modes

- READ: shows card info on OLED
- DOOR: checks authorization; if authorized, raises `D7` for ~5 seconds
- ADD: scans are added to the authorized list (EEPROM)
- REMOVE: scans are removed from the authorized list (EEPROM)

You can switch modes in two ways:
1) Scan the Admin card to cycle READ → DOOR → ADD → REMOVE → READ
2) Scan one of these pre-programmed “mode cards” (Wiegand raw):
- READ: `0x1E6DC032`
- DOOR: `0x1E6D9861`
- ADD: `0x1E6DFCA0`
- REMOVE: `0x1E6DE636`

## Serial Logging (optional)

Serial is disabled by default. To enable:

```cpp
#define ENABLE_SERIAL 1
```

Then open the Serial Monitor at 115200 baud to see card scans and actions.

## EEPROM Layout (summary)

- `[0]` card count (1 byte)
- `[4..]` authorized cards table (`MAX_CARDS * 4` bytes, little-endian)
- `EEPROM_ADMIN_UID_ADDR` (4 bytes): admin UID
- `EEPROM_ADMIN_FLAG_ADDR` (1 byte): `0xA5` when admin enrolled

To reset admin or clear all authorized cards, wipe the EEPROM (e.g., with a small Arduino sketch) and reboot to re-enroll.

## Building/Flashing

- Board: Arduino Nano (select the correct processor/old bootloader if required by your unit)
- Open `HIDR10-RP15-Door-Simulator/HIDR10-RP15-Door-Simulator.ino`
- Install required libraries: `Adafruit_GFX`, `Adafruit_SSD1306`, `Wiegand`, `EEPROM`
- Wiegand library: `monkeyboard/Wiegand-Protocol-Library-for-Arduino` (`https://github.com/monkeyboard/Wiegand-Protocol-Library-for-Arduino`)
- Compile and upload via USB directly to the Arduino (the in-case USB-C pigtail is power-only)

## Notes

- The Nano build intentionally omits WiFi/web UI/REST/HA integrations. See the repository root README for the ESP32 variant if you need those features.