Делаем RFID замок своими руками

Автор: | 18.07.2021

В сети есть много уроков о том как подключить считыватель RFID-меток к плате Arduino. Но обычно они заканчиваются считыванием идентификатора метки и простеньким условием открывания замка, в котором разрешённые метки прописаны прямо в скетче.

Но что если мы хотим создать замок и установить его на дверь. Довольно неудобно каждый раз при необходимости добавить/удалить ключ вытаскивать ардуинку из замка или подключать к ней ноутбук или компьютер.

ВНИМАНИЕ! Не устанавливайте самодельный замок на двери за которыми находится ценное имущество. Это очень ненадежный способ защиты от проникновения.

Наш замок подойдёт например для калитки, межкомнатной двери, для клетки с хомячком. Использование его на входной двери в квартире небезопасно!

Схема подключения

Итак, начнем со схемы подключения. Будем использовать распространенный и дешевый модуль RC522.

На схеме нарисована плата Arduino Nano, я выбрал ее потому что это самая компактная плата с выходом USB. Но подойдет и любая другая плата Arduino.

Кнопку для стирания ключей (“секретную кнопку”) подключаем одним контактом к D2, а вторым – на землю (GND). Кнопка должна быть нормально-разомкнутой.

Логика работы

Очень часто в сети описывается замок, ключи для которого изначально прописаны в скетче. Логика работы такого скетча очень проста, но обслуживать такой замок крайне неудобно. Представьте, что Вам потребовалось добавить новый ключ или удалить один из действующих. Для этого необходимо будет вытащить внутренности замка, подключиться к Arduino при помощи ноутбука или компьютера, узнать номер ключа-карты (он не всегда написан на самом ключе), изменить скетч и залить его в плату Arduino.

Предлагаю обойтись без физического доступа к плате Arduino, а для добавления и удаления ключей использовать так называемый “мастер-ключ”. Все ключи (в том числе и мастер-ключ) будем хранить в энергонезависимой памяти (EEPROM). Это позволит изменять базу данных ключей без изменения скетча. Еще предусмотрим “секретную кнопку”, которая бы позволила стереть всю базу данных или заменить мастер ключ.

Итак, если база данных ключей пустая – первый считанный ключ становится мастер-ключом. Мастер ключ не открывает замок, но позволяет производить действия с базой ключей. При поднесении к считывателю мастер-ключа система переходит в режим программирования. Для выхода из этого режима нужно повторно поднести мастер-ключ.

Если в режиме программирования к считывателю поднести ключ, которого нет в базе разрешенных ключей – он запишется в энергонезависимую память и будет открывать замок. Если поднести ключ, который уже присутствует в базе – он удалится из памяти. При добавлении и удалении ключей в мониторе порта отображаются соответствующие сообщения.

Если в рабочем режиме поднести карту к считывателю – осуществляется поиск ее идентификатора в базе EEPROM. Если карта найдена – замок откроется, если нет – останется закрытым.

Для стирания всей базы ключей нужно при перезагрузке Arduino держать нажатой секретную кнопку в течение 10 секунд. Чтобы стереть мастер ключ – нужно в процессе работы нажать секретную кнопку на 10 секунд.

Скетч

Скетч позаимствован мной из примеров библиотеки Arduino RFID для MFRC522. Разработчики библиотеки удалили его из примеров по какой-то причине (31 июля 2021 г., в версии библиотеки 1.4.9). Я же сохранил этот скетч и внес изменения, необходимые для своего проекта. Привожу скетч с подробными комментариями.

#include <EEPROM.h>     // Библиотека EEPROM
#include <SPI.h>        // Модуль RC522 использует SPI протокол
#include <MFRC522.h>    // Библиотека для Mifare RC522 устройства

#define relay 4     // пин для подключения реле
#define wipeB 2     // кнопка для стирания базы ключей

bool programMode = false;  // флаг входа в программный режим
bool successRead;    // флаг успешного считывания ID карты

byte storedCard[4];   // ID простой карты, прочитанной из EEPROM
byte readCard[4];   // ID карты, полученной от RFID-модуля
byte masterCard[4];   // SID мастер-карты, прочитанной из EEPROM

#define SS_PIN 10 //SS-пин (смотри схему подключения)
#define RST_PIN 9 //RST-пин (смотри схему подключения)
MFRC522 mfrc522(SS_PIN, RST_PIN); //создание экземпляра класса MFRC522

void setup() {
  pinMode(wipeB, INPUT_PULLUP); // подтяжка кнопки к напряжению питания
  pinMode(relay, OUTPUT); // конфигурация пина реле как "выход"
  digitalWrite(relay, LOW); // низкий логический уровень на реле

  Serial.begin(9600);  // инициализация монитора порта на скорости 9600 бод
  SPI.begin();         // инициализация протокола SPI для работы с MFRC522
  mfrc522.PCD_Init();  // инициализация платы MFRC522

  if (!digitalRead(wipeB)) {  // если нажата кнопка wipeB (соединена с GND)
    Serial.println(F("Wipe Button Pressed")); //кнопка стирания базы нажата
    Serial.println(F("You have 10 seconds to Cancel")); // у Вас 10 секунд для отмены
    Serial.println(F("This will be remove all records and cannot be undone")); //все записи будут удалены безвозвратно
    bool buttonState = monitorWipeButton(10000); // даём 10 секунд, чтобы отпустить кнопку
    if (buttonState == true && !digitalRead(wipeB)) {    // если кнопка нажата - стираем EEPROM
      Serial.println(F("Starting Wiping EEPROM"));
      for (uint16_t x = 0; x < EEPROM.length(); x++) {  //цикл по всем адресам EEPROM
        EEPROM.update(x, 0); // заполняем ячейки EEPROM нулями
      }
      Serial.println(F("EEPROM Successfully Wiped")); //EEPROM успешно стёрта
      delay(1000);
    }
    else {
      Serial.println(F("Wiping Cancelled")); // Сообщение об отмене стирания
    }
  }
  // Проверим задан ли мастер-ключ, для этого прочитаем ячейку один из EEPROM
  // Если мастер-ключ задан - в ячейке будет число 143,
  // если там другое значение - значит надо задать мастер-ключ.
  // Для того чтобы заменить мастер-ключ на новый, 
  // запишите в ячейку 1 любое значение, отличное от 143
  if (EEPROM.read(1) != 143) {
    Serial.println(F("No Master Card Defined")); //мастер-карта не задана
    Serial.println(F("Scan A PICC to Define as Master Card")); //приложите карту к считывателю
    do {
      successRead = false; //флаг успешного чтения ID карты
      successRead = getID(true); // ставим флаг в true при получении ID карты
      delay(1000);
    }
    while (!successRead); // программа не будет работать дальше без успешного считывания мастер-карты
    for ( uint8_t j = 0; j < 4; j++ ) {        // записываем в цикле четыре байта
      EEPROM.write( 2 + j, readCard[j] );  // пишем считанный UID в EEPROM, начиная с ячейки 2
    }
    EEPROM.write(1, 143);                  // Пишем флаг наличия мастер-карты в EEPROM.
    Serial.println(F("Master Card Defined"));
  }
  Serial.println(F("-------------------"));
  Serial.println(F("Master Card's UID"));
  for ( uint8_t i = 0; i < 4; i++ ) { // Читаем UID из EEPROM
    masterCard[i] = EEPROM.read(2 + i);    // пишем в массив masterCard
    Serial.print(masterCard[i], HEX);
  }
  Serial.println("");
  Serial.println(F("-------------------"));
  Serial.println(F("Everything is ready"));
  Serial.println(F("Waiting PICCs to be scanned"));
}

void loop () {
  do {
    successRead = getID(programMode);  //  функция возвращает true при успешном считывании карты
    // Стираем мастер карту если кнопка нажата более 10 секунд. 
    if (!digitalRead(wipeB)) { // проверяем, что кнопка нажата
      Serial.println(F("Wipe Button Pressed")); //Сообщение в монитор порта
      Serial.println(F("Master Card will be Erased! in 10 seconds")); //предупреждение
      bool buttonState = monitorWipeButton(10000); // Дадим пользователю время одуматься
      if (buttonState && !digitalRead(wipeB)) {    // Если кнопка все ещё нажата
        EEPROM.write(1, 0);                  // Сбрасываем содержимое ячейки 1
        Serial.println(F("Master Card Erased from device"));
        Serial.println(F("Please reset to re-program Master Card"));
        while (1); //бесконечный цикл, чтобы программа не выполнялась дальше
      }
      Serial.println(F("Master Card Erase Cancelled")); //Сообщение об отмене, если кнопку отпустили
    }
  }
  while (!successRead);   //программа не будет работать дальше без успешного считывания карты
  if (programMode) {
    if ( isMaster(readCard) ) { //если мы в режиме программирования поднесем мастер-карту
      Serial.println(F("Master Card Scanned"));
      Serial.println(F("Exiting Program Mode")); //Сообщение о выходе из режима программирования
      Serial.println(F("-----------------------------"));
      programMode = false;
      delay(5000);
      return;
    }
    else {
      if ( findID(readCard) ) { // Если карта есть в базе - удаляем
        Serial.println(F("I know this PICC, removing..."));
        deleteID(readCard);
        Serial.println("-----------------------------");
        Serial.println(F("Scan a PICC to ADD or REMOVE to EEPROM"));
      }
      else {                    // Если карты нет в базе - добавляем
        Serial.println(F("I do not know this PICC, adding..."));
        writeID(readCard);
        Serial.println(F("-----------------------------"));
        Serial.println(F("Scan a PICC to ADD or REMOVE to EEPROM"));
      }
    }
  }
  else {
    if ( isMaster(readCard)) {    // если считана Мастер-карта - входим в режим программирования
      programMode = true;
      Serial.println(F("Hello Master - Entered Program Mode"));
      uint8_t count = EEPROM.read(0);   // Читаем ячейку 0 EEPROM в которой хранится количество карточек в базе
      Serial.print(F("I have ")); //выводим количество в монитор порта
      Serial.print(count);
      Serial.print(F(" record(s) on EEPROM"));
      Serial.println("");
      Serial.println(F("Scan a PICC to ADD or REMOVE to EEPROM"));
      Serial.println(F("Scan Master Card again to Exit Program Mode"));
      Serial.println(F("-----------------------------"));
    }
    else {
      if ( findID(readCard) ) { // если обычная карта найдена в EEPROM
        Serial.println(F("Welcome, You shall pass"));
        unlockDoor();         // Открываем замок
      }
      else {      // если обычная карта не найдена в EEPROM
        Serial.println(F("You shall not pass"));
        lockDoor(); //Закрываем замок
      }
    }
  }
}

//Функция открывания двери (можно изменить в зависимости от типа замка)
void unlockDoor() {
  digitalWrite(relay, HIGH);     // Открываем дверь
  Serial.println("Relay pin is HIGH");
}

//Функция закрывания двери (можно изменить в зависимости от типа замка)
void lockDoor() {
  digitalWrite(relay, LOW); //Закрываем дверь
  Serial.println("Relay pin is LOW");
  delay(1000);
}


//Функция получения ID ключа (карты)
bool getID(bool programMode) {
  if (!programMode) {
    byte atqa_size = 2;
    byte atqa_answer[2];
    mfrc522.PICC_WakeupA(atqa_answer, &atqa_size); //необходимо для повторного считывания карты
  } else {
    mfrc522.PICC_IsNewCardPresent();
  }
  if (! mfrc522.PICC_ReadCardSerial()) {
    lockDoor();     // закрываем дверь
    return false;
  }
  // Существуют карты Mifare, которые имеют 4-байтовый или 7-байтовый UID,
  // Для упрощения будем считать, что все карты 4-байтовые
  // Пока что мы не будем поддерживать 7-байтовые карты
  Serial.println(F("Scanned PICC's UID:"));
  for ( uint8_t i = 0; i < 4; i++) {  //
    readCard[i] = mfrc522.uid.uidByte[i]; //считываем байт
    Serial.print(readCard[i], HEX); //выводим в монитор порта
  }
  Serial.println("");
  mfrc522.PICC_HaltA(); // останавливаем считывание
  return true;
}

//Функция чтения ID из EEPROM 
void readID( uint8_t number ) {
  uint8_t start = (number * 4 ) + 2;    // вычисляем начальную ячейку EEPROM
  for ( uint8_t i = 0; i < 4; i++ ) {     // Цикл для получения 4 байт
    storedCard[i] = EEPROM.read(start + i);   // Присвоение значений, считанных из EEPROM, массиву
  }
}

// Функция добавления ID в EEPROM
void writeID( byte a[] ) {
  if ( !findID( a ) ) {     // Перед записью в EEPROM, проверяем не добавлена ли она ранее
    uint8_t num = EEPROM.read(0);     // Получаем количество карточек, которое хранится в ячейке 0 EEPROM
    uint8_t start = ( num * 4 ) + 6;  // Вычисляем начальную ячейку для записи в EEPROM
    num++;                // Добавляем одну карточку к счетчику
    EEPROM.write( 0, num );     // Записываем новое количество в EEPROM
    for ( uint8_t j = 0; j < 4; j++ ) {   //Цикл для записи
      EEPROM.write( start + j, a[j] );  // Записываем массив с ID в EEPROM
    }
    delay(1000);
    Serial.println(F("Succesfully added ID record to EEPROM")); //Успешно
  }
  else {
    delay(1000);
    Serial.println(F("Failed! There is something wrong with ID or bad EEPROM")); //Ошибка
  }
}

// Функция удаления ID из EEPROM
void deleteID( byte a[] ) {
  if ( !findID( a ) ) {     // Перед удалением из EEPROM, проверяем была ли она добавлена ранее
    delay(1000);    // Если нет
    Serial.println(F("Failed! There is something wrong with ID or bad EEPROM")); //Ошибочка вышла
  }
  else {
    uint8_t num = EEPROM.read(0);   // Получаем количество карточек, которое хранится в ячейке 0 EEPROM
    uint8_t slot = findIDSLOT( a );  // Находим номер начальной ячейки удаляемой карты
    uint8_t start = (slot * 4) + 2;  // Вычисляем начальную ячейку
    uint8_t looping = ((num - slot) * 4); // количество повторений цикла перезаписи
    num--;      // Уменьшаем счетчик карт на одну
    EEPROM.write( 0, num );   // Записываем новый счетчик в EEPROM
    for (uint8_t j = 0; j < looping; j++ ) { // Цикл сдвига карт на место удаленной
      EEPROM.write( start + j, EEPROM.read(start + 4 + j));   //Сдвигаем все значения на 4 байта раньше
    }
    for ( uint8_t k = 0; k < 4; k++ ) {         // Сдвигающий цикл
      EEPROM.write( start + j + k, 0);
    }
    delay(1000);
    Serial.println(F("Succesfully removed ID record from EEPROM")); //Успешно
  }
}

// Побайтовое сравнение IDшников
bool checkTwo ( byte a[], byte b[] ) {
  for ( uint8_t k = 0; k < 4; k++ ) {   // цикл с четырьмя итерациями
    if ( a[k] != b[k] ) {   // если a != b возвращаем false, т.к. ID разные
      return false;
    }
  }
  return true;
}

// Находим слот в котором хранится ID карты
uint8_t findIDSLOT( byte find[] ) {
  uint8_t count = EEPROM.read(0); // Считываем количество карт из EEPROM
  for ( uint8_t i = 1; i <= count; i++ ) {  // цикл для каждой карты в EEPROM
    readID(i); // читаем ID из EEPROM, он сохранится в массиве storedCard[4]
    if ( checkTwo( find, storedCard ) ) {   // Проверяем совпадает ли карта, считанная из EEPROM с картой переданной в аргументе find[]
      return i;         // Если совпало - возвращаем номер слота карты
    }
  }
}

// Находим ID в EEPROM
bool findID( byte find[] ) {
  uint8_t count = EEPROM.read(0);     // Считываем количество карт из EEPROM
  for ( uint8_t i = 1; i < count; i++ ) { // цикл для каждой карты в EEPROM
    readID(i);  // читаем ID из EEPROM, он сохранится в массиве storedCard[4]
    if ( checkTwo( find, storedCard ) ) {   // Проверяем совпадает ли карта, считанная из EEPROM
      return true;
    }
  }
  return false; // если не совпадает возвращаем false
}

// Проверяем является ли карта мастер-картой
bool isMaster( byte test[] ) {
  return checkTwo(test, masterCard);
}

// Отслеживаем нажатие кнопки
bool monitorWipeButton(uint32_t interval) {
  uint32_t now = (uint32_t)millis();
  while ((uint32_t)millis() - now < interval)  {
    // проверяем каждые полсекунды
    if (((uint32_t)millis() % 500) == 0) {
      if (digitalRead(wipeB))
        return false;
    }
  }
  return true;
}

Пишите свои замечания в комментариях, буду рад конструктивной критике.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *