Сравните цены на arduino uno nfc — мегаскидки на arduino uno nfc от продавцов со всего мира на АлиЭкспресс

Rfid система контроля доступа для дверного замка

Давайте создадим небольшой проект на Arduino, чтобы продемонстрировать, как простой модуль RFID считывателя RC522 можно использовать для создания RFID системы контроля доступа для дверного замка. Наша программа будет сканировать уникальный идентификатор каждой RFID метки, когда она достаточно близко, чтобы запитываться от считывателя RC522.

Так выглядит результат.

Рисунок 13 Демонстрация работы RFID системы контроля доступа для дверного замка
Рисунок 13 – Демонстрация работы RFID системы контроля доступа для дверного замка

Конечно, этот проект можно привязать к открытию дверей, включению реле, включению светодиода или к чему-то еще.

Если вы не знакомы с символьными LCD дисплеями размером 16×2, то взгляните на эту статью.

Прежде чем мы перейдем к загрузке кода и сканированию меток, давайте посмотрим на принципиальную схему проекта.

Рисунок 14 RFID система контроля доступа для дверного замка. Подключение RFID считывателя RC522 и LCD дисплея к Arduino
Рисунок 14 – RFID система контроля доступа для дверного замка. Подключение RFID считывателя RC522 и LCD дисплея к Arduino

Всё! Теперь попробуйте приведенный ниже скетч в работе.

#include <SPI.h>
#include <MFRC522.h>
#include <LiquidCrystal.h>

#define RST_PIN 9
#define SS_PIN 10

byte readCard[4];
String MasterTag = "20C3935E";	// ЗАМЕНИТЕ этот ID метки на ID своей метки!!!
String tagID = "";

// Создание объектов
MFRC522 mfrc522(SS_PIN, RST_PIN);
LiquidCrystal lcd(7, 6, 5, 4, 3, 2); //Параметры: (rs, enable, d4, d5, d6, d7) 

void setup() 
{
  // Инициализация
  SPI.begin();        // SPI шина
  mfrc522.PCD_Init(); // MFRC522
  lcd.begin(16, 2);   // LCD дисплей

  lcd.clear();
  lcd.print(" Access Control ");
  lcd.setCursor(0, 1);
  lcd.print("Scan Your Card>>");
}

void loop() 
{
  
  // Ждем, пока не будет доступна новая метка
  while (getID()) 
  {
    lcd.clear();
    lcd.setCursor(0, 0);
    
    if (tagID == MasterTag) 
    {
      lcd.print(" Access Granted!");
      // Вы можете написать здесь любой код, например, открывание дверей,
      // включение реле, зажигание светодиода или что-то другое, что взбредет вам в голову.
    }
    else
    {
      lcd.print(" Access Denied!");
    }
    
    lcd.setCursor(0, 1);
    lcd.print(" ID : ");
    lcd.print(tagID);
      
    delay(2000);

    lcd.clear();
    lcd.print(" Access Control ");
    lcd.setCursor(0, 1);
    lcd.print("Scan Your Card>>");
  }
}

// Чтение новой метки, если она доступна
boolean getID() 
{
  // Получение готовности для чтения PICC карт
  if ( ! mfrc522.PICC_IsNewCardPresent()) 
  { // Продолжать, если к RFID считывателю поднесена новая карта
    return false;
  }
  
  if ( ! mfrc522.PICC_ReadCardSerial()) 
  { // Когда карта поднесена, считать серийный номер и продолжить
    return false;
  }
  
  tagID = "";
  for ( uint8_t i = 0; i < 4; i  ) 
  { // Карты MIFARE, кторые мы используем, содержат 4-байтовый UID
    //readCard[i] = mfrc522.uid.uidByte[i];
    tagID.concat(String(mfrc522.uid.uidByte[i], HEX)); // Сложить эти 4 байта в одну переменную String
  }
  tagID.toUpperCase();
  mfrc522.PICC_HaltA(); // остановить чтение
  return true;
}

Программа довольно проста. Сначала мы включаем необходимые библиотеки, определяем выводы Arduino, создаем объекты LCD и MFRC522 и определяем главную метку.

#include <SPI.h>
#include <MFRC522.h>
#include <LiquidCrystal.h>

#define RST_PIN 9
#define SS_PIN 10

byte readCard[4];
String MasterTag = "20C3935E";	// ЗАМЕНИТЕ этот ID метки на ID своей метки!!!
String tagID = "";

// Создание объектов
MFRC522 mfrc522(SS_PIN, RST_PIN);
LiquidCrystal lcd(7, 6, 5, 4, 3, 2); //Параметры: (rs, enable, d4, d5, d6, d7) 

В функции setup() мы инициализируем интерфейс SPI, объект MFRC522 и LCD дисплей. После этого мы печатаем на LCD дисплее приветственное сообщение.

void setup() 
{
  // Инициализация
  SPI.begin();        // SPI шина
  mfrc522.PCD_Init(); // MFRC522
  lcd.begin(16, 2);   // LCD дисплей

  lcd.clear();
  lcd.print(" Access Control ");
  lcd.setCursor(0, 1);
  lcd.print("Scan Your Card>>");
}

В функции loop() мы ждем, пока не будет отсканирована новая метка. Как только это будет сделано, мы сравним неизвестную метку с мастер-меткой, определенной в функции setup(). Всё! Если ID метки совпадает с ID мастера, доступ предоставляется, в противном случае в доступе будет отказано.

void loop() 
{
  
  // Ждем, пока не будет доступна новая метка
  while (getID()) 
  {
    lcd.clear();
    lcd.setCursor(0, 0);
    
    if (tagID == MasterTag) 
    {
      lcd.print(" Access Granted!");
      // Вы можете написать здесь любой код, например, открывание дверей,
      // включение реле, зажигание светодиода или что-то другое, что взбредет вам в голову.
    }
    else
    {
      lcd.print(" Access Denied!");
    }
    
    lcd.setCursor(0, 1);
    lcd.print(" ID : ");
    lcd.print(tagID);
      
    delay(2000);

    lcd.clear();
    lcd.print(" Access Control ");
    lcd.setCursor(0, 1);
    lcd.print("Scan Your Card>>");
  }
}

Ключевым моментом в проекте является пользовательская функция getID(). Как только она просканирует новую карту, внутри цикла for она преобразует 4 байта UID в строки и объединяет их для создания одной строки.

boolean getID() 
{
  // Получение готовности для чтения PICC карт
  if ( ! mfrc522.PICC_IsNewCardPresent()) 
  { // Продолжать, если к RFID считывателю поднесена новая карта
    return false;
  }
  
  if ( ! mfrc522.PICC_ReadCardSerial()) 
  { // Когда карта поднесена, считать серийный номер и продолжить
    return false;
  }
  
  tagID = "";
  for ( uint8_t i = 0; i < 4; i  ) 
  { // Карты MIFARE, кторые мы используем, содержат 4-байтовый UID
    //readCard[i] = mfrc522.uid.uidByte[i];
    tagID.concat(String(mfrc522.uid.uidByte[i], HEX)); // Сложить эти 4 байта в одну переменную String
  }
  tagID.toUpperCase();
  mfrc522.PICC_HaltA(); // остановить чтение
  return true;
}

Оригинал статьи:

Замок с радиочастотной идентификацией на базе nfc контроллера pn532

Привет друзья.

В данной теме пойдет речь о конфигурации микроконтроллера через UART (Universal Asynchronous Receiver-Transmitter) интерфейс. А рассмотрим мы это на примере MQTT логгера. В данном случае, это будет логгер температуры. Мне это устройство потребовалось на работе, даже не мне, а моим коллегам, и оно действительно работает и приносит огромную пользу т.к контроль температуры производится совместно с отличной, на мой взгляд, системой мониторинга Zabbix с оперативными оповещениями, построением графиков, блэк-джеком и… Подробнее о дружбе Arduino и Zabbix можно почитать тут

Но как всегда, есть нюансы. А заключаются они в том, что в будущем, обслуживать армию мелких контроллеров придется людям, которые заняты своими задачами и им попросту некогда изучать Arduino, не говоря уже о серьезных альтернативах, разбираться в том, как прописать нужные значения переменных в программу и загрузить её в микроконтроллер. Все настройки необходимо производить быстро, с явным указанием изменяемого параметра и его значения. Ровно также, как это делается с любым промышленным оборудованием.

И тут на помощь приходит UART

Микросхема UART to USB имеется в большинстве плат семейства Arduino, а там, где её нет, обычно выведены соответствующие “пины”. И все это очень облегчает жизнь т.к позволяет общаться с контроллером, просто подключив его к компьютеру напрямую или через переходник, благо их везде навалом, да и стоят они как пачка семечек. Остается только запустить любой терминал, который умеет доставлять в конец строки символ “перевод строки”, что известен в народе как “n”, а в ASCII таблице имеет номер 0A.

Кстати, в Serial мониторе Arduino IDE выставить символ конца строки можно так

Ну а дальше только, что и остается, как общаться с устройством на той стороне. И тут мы переходим к основному алгоритму программы. Но перед этим хочу отметить, и это ВАЖНО, что за любое упрощение жизни, всякие красивости и прочее, приходиться платить, и цена довольно высока! В данном случае, это ОЗУ микроконтроллера. Поэтому не раскатываем губы, а если очень хочется, то берем следующий по характеристикам микроконтроллер. А начинать мы будем с ATmega328P, что известен в народе как Arduino UNO, Arduino Nano, IBoard v1.1 и т.д по списку. Заканчивать Вы можете чем угодно, хоть ATmega2560, ESP8266 или ESP32. В противном случае, производим оптимизацию кода, отказываемся от громоздких библиотек, или вообще, от Arduino IDE.

Что мы хотим получить

Вся конфигурация микроконтроллера должна храниться в энергонезависимой памяти (ПЗУ) известной нам как EEPROMM.

Если в ПЗУ конфигурация отсутствует, необходимо иметь резервный план. И им станет сброс конфигурации на настройки по умолчанию. Это поведение знакомо всем, особенно по домашним дешевым маршрутизаторам, а значит, интуитивно понятно.

Выводить справку при начале общения пользователя и устройства, на мой взгляд, как манеры высшего общества. Контроллер должен представляться и сообщать всю необходимую информацию о себе и о том, как с ним вести диалог.

Все команды должны быть просты и иметь не двусмысленное значение.

И конечно, мы должны иметь возможность просмотра текущего состояния датчиков или процессов, которыми занимается устройство в свободное от общения с нами время.

Как сохранять конфигурацию в EEPROM

Пожалуй, стоит начать с того, как сохранить конфигурацию микроконтроллера в энергонезависимую память. Для этих целей, в стандартный набор инструментов Arduino IDE входит библиотека для работы с EEPROM.

#include <EEPROM.h>

На данный момент нас интересуют две функции, это чтение и запись

EEPROM.get(address, variable);
EEPROM.put(address, variable);

Обе принимают два параметра:

Адрес, начиная с которого будет произведено чтение или запись данных в память

Переменная чье содержимое надо сохранить или в которую нужно из памяти прочитать

Особенность работы этих функция заключается в том, что в зависимости от типа переданной им переменной во втором параметре, будет произведено чтение или запись ровно того количества данных которое соответствует размеру типа этой самой переменной. На простом языке это означает, что если переменная variable будет иметь типа byte, то и работать мы будем с объемом памяти в 1 байт. И тоже самое произойдет с абсолютно любым типом данных пока мы не упремся в размеры самого EEPROM или ОЗУ микроконтролера. Из этого всего следует, что мы можем создать свой собственный тип данных, разместить в нем необходимую нам информацию и всего лишь двумя функциями помещать его в память и извлекать обратно.

И в этом нам поможет пользовательский составной тип – структура (struct). Данный тип позволяет объединить в себе различные типы данных, упорядочить их и присвоить им понятные имена.

Это общий пример для большего понимания, как объединить несколько типов данных в одной структуре, получить к ним доступ, записать и прочитать их из EEPROM.

Наша структура будет немного сложнее, но суть остается той же самой.

// Дополнительная структура описывающая IPv4 адреса
struct addres {
byte a;
byte b;
byte c;
byte d;
};

Читайте ещё про NFC:  Можно ли деньги со стима перевести на киви и как это сделать

// Структура объекта конфига для хранения в EEPROM
struct configObj {
addres ip;
addres subnet;
addres gateway;
addres dns;
byte mac[6];
byte hex;
char server[40];
char topic[40];
} config;

Данная структура хранит сетевые настройки для работы с Ethernet модулем (w5100 и выше) Arduino, базовые настройки для связи с MQTT брокером. Сразу при описании структуры мы объявили новую переменную с именем config с типом нашей структуры.

ВАЖНО: кроме наших данных в структуре имеется дополнительная переменная с именем hex. Её задача, это контроль наличия наших данных в EEPROM. Она всегда должна содержать одно и тоже значение. Представьте ситуацию, что вы взяли контроллер в EEPROM которого находится какая-либо информация (может там чисто, но мы этого не знаем наверняка) и мы прочитаем данные и поместим их в нашу переменную. В итоге мы получим данные которым нельзя доверять, а что еще хуже, это если эти самые данные нарушат работу внешнего оборудования.

Более правильным, на мой взгляд, будет проверка значений по конкретно определенным адресам. Например, мы знаем, что в 16 байте должно быть значение 0xAA и если оно действительно там, то мы убеждаемся, что это наша информация. Естественно, что контрольных точек может быть несколько и разумеется с разными значениями, это увеличит гарантию того, что данные являются нашими, но 100% гарантии не даст. Для более серьезных проектов есть более серьезные методы, например, подсчет контрольной суммы всего набора данных.

Также структура может иметь вложенные структуры, у нас ими являются: ip, subnet, gateway, dns. Вы можете отказаться от такого варианта и записывать данные просто в массив байт, как это было сделано с MAC адресом. Естественно, что обращаться к этим полям нужно по-разному.

Запись данных в поле subnet

config.subnet = {255, 255, 255, 0};

Запись данных в поле mac

byte mac[] = {0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02};
memcpy(config.mac, mac, 6);

С записью данных в поле server все еще проще

config.server = “mqtt.nfcexpert.ru”;

Функция, которая возвращает нашу структуру данных с полностью заполненными полями.

// Начальный конфиг
configObj defaultConfig() {
configObj config = {
{192, 168, 0, 200},
{255, 255, 255, 0},
{192, 168, 0, 1},
{192, 168, 0, 1},
{0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02},
0xAA, // Не трогать! Используется для проверки конфигурации в EEPROM
F(“mqtt.nfcexpert.ru”),
F(“arduino/serial/config”)
};
return config;
}

К примеру, два последних значения записывать не обязательно, тогда эти поля останутся пусты если использовать данную функцию для возврата к “заводским” настройкам.

Вот пример того, как используя описанную нами структуру, мы проверяем целостность настроек в EEPROM и в случае не совпадения hex значений, загружаем настройки по умолчанию.

const byte startingAddress = 9;
bool configured = false;

void loadConfig() {
EEPROM.get(startingAddress, config);
if (config.hex == 170) configured = true;
else config = defaultConfig();
configEthernet(); // Функция производящая настройку сети
}

Как контроллеру начать понимать, что от него хотят

В Arduino имеется функция, вызываемая каждый раз, когда в передаваемый буфер данных попадает знакомый нам символ перевода строки.

void serialEvent() {
// Вызывается каждый раз, когда что-то прилетает по UART
// Данные передаются посимвольно. Если в строке 100 символов, то функция будет вызвана 100 раз
}

И в контексте обсуждаемой нами программы, мы можем представить ее в следующем виде

void serialEvent() {
  serialEventTime = millis();
  if (console.available()) {
    char c = (char)console.read();
    if (inputCommands.length() < inputCommandsLength) {
      if (c != ‘n’) inputCommands = c;
      else if (inputCommands.length()) inputCommandsComplete = true;
    }
  }
}

Её задача, символ за символом, собрать в кучу все переданные нами данные и при получении заветного символа перевода строки (именно он даст нам понять, что передача сообщения завершена) сообщить, что команда получена и передать накопленный буфер данных своей напарнице по цеху.

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

Останется только избавиться от них, и самым удобным моментом будет, когда этот поток шлака прекратиться. Чтобы об этом узнать мы будем запоминать время, когда пришел каждый из символов переданной строки перезаписывая соответствующую временную переменную данными о следующем символе и т.д пока поток не иссякнет. И как только расхождение текущего времени CPU и времени, когда поступил последний символ превысит некоторое значение, пусть это будет 1 секунда, мы очистим нашу память. Этот простой механизм напоминающий амнезию позволит избавить нас от лишних проблем.

Переменная отвечающая за размер принимаемого буфера

const byte inputCommandsLength = 60;

Теперь можно переходить к напарнице предыдущей героини и, по совместительству, основной рабочей лошадки – функции обрабатывающей адекватно полученные данные.

void serialEventHandler() {
// вызывается в loop и проверяет взведена ли переменная inputCommandsComplete
// в полученных данных пытается распознать команды
}

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

Разбор serialEventHandler

Полученные данные будут переданы нам в переменной inputCommands с типом String

В первую очередь стоит почистить ее от лишних пробельных символов. Они часто встречаются в начале и в конце строки если пользователь копирует текст, а не набирает его самостоятельно. Это распространенная ситуация, приводящая к отказу принятия команды и бороться с ней очень просто.

inputCommands.trim();

Далее стоит отсеять команды, не несущие никакой динамической информации, например, help, restart, reset и т.п это предписывающие команды которые заставляют контроллер выполнять строго описанные функции без вмешательства в их работу.

if (inputCommands == F(“help”)) {
consoleHelp();
} else if (inputCommands == F(“restart”)) {
resetFunc();
} else {
// Все сложные команды обрабатываются в этом блоке
}

Как Вы видите, все очень просто и скучно. Но не в том случае если команда динамическая, то есть содержит не только саму команду (заголовок) но и полезную нагрузку (параметр) которая может меняться раз от раза. Простой пример это команда изменения ip адреса и её варианты:

ip 37.140.198.90

ip 192.168.0.244

ip 10.10.10.88

В данном случае, нам стоит понять, относится ли данная команда именно к ip адресу. Для этого в наборе String имеется отличный метод, позволяющий производить сравнение переданного ему параметра с началом строки.

if (inputCommands.startsWith(F(“ip”))) {
// Строка inputCommands начинается с пары символов “ip”
}

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

inputCommands.substring(4)

В данном случае начиная с 4-его и заканчивая последним. И как Вы успели заметить, отсчет мы начинаем не с третьего символа, что соответствует нашей строке без вступительного “ip”, а на один больше т.к между заголовком и параметром имеется разделяющий символ в виде пробела.

Далее, полученную строку мы передадим в функцию, занимающуюся разбором на компоненты и принимающую следующие параметры:

Указатель на переменную с типом char, для этого нам потребуется преобразовать наш тип String

Символ разделителя, что для IPv4 является точка “.”

Указатель на массив типа byte, которому будет присвоен результат разбора

Количество искомых элементов в строке

И система счисления, подразумеваемая в качестве исходной для записи элементов подстроки

/*
Парсинг
https://stackoverflow.com/questions/35227449/convert-ip-or-mac-address-from-string-to-byte-array-arduino-or-c
*/
void parseBytes(const char* str, char sep, byte* bytes, int maxBytes, int base) {
for (int i = 0; i < maxBytes; i ) {
bytes[i] = strtoul(str, NULL, base);
str = strchr(str, sep);
if (str == NULL || *str == ”) break;
str ;
}
}

В нашем случае выглядеть это будет следующим образом

byte ip[4];
parseBytes(inputCommands.substring(4).c_str(), ‘.’, ip, 4, 10);

А дале все становится еще проще, попросту проверить попадает ли наш ip адрес, в список правильных адресов. И самой простой проверкой послужит проверка первого байта адреса на несоответствие не угодным нам сетям (0, 127, 255)

if (ip[0] != 127 and ip[0] != 255 and ip[0] != 0) {
// Производим необходимые нам действия с ip адресом, например, запись в конфиг
config.ip = {ip[0], ip[1], ip[2], ip[3]};
}

Вы в праве реализовать собственные проверки, какие только душе угодны.

Также хотелось бы отметить, что обрабатывать некоторые параметры проще и быстрее через их короткие записи. К таким можно отнести маску подсети устройства. Например, привычный дня нас адрес 192.168.0.1 с маской подсети 255.255.255.0 можно записать в виде 192.168.0.1/24, где цифра 24 указывает нашу подсеть в краткой форме. А, следовательно, мы можем записать несколько кратких форм масок подсети в следующем виде:

subnet 255.255.255.0 или subnet 24

Читайте ещё про NFC:  Технология RFID что это такое? Где применяется RFID модуль (чип) и его отличие от NFC.

subnet 255.255.0.0 или subnet 16

subnet 255.0.0.0 или subnet 8

Это основные маски, и я не описывал все существующие т.к в этом нет нужды, но если Вам интересно, то почитать про них можно в wikipedia.

if (inputCommands.startsWith(F(“subnet”))) {
    String input = inputCommands.substring(8);
    if (input == F(“24”))      config.subnet = {255, 255, 255,   0};
    else if (input == F(“16”)) config.subnet = {255, 255,   0,   0};
    else if (input == F(“8”))  config.subnet = {255,   0,   0,   0};
    else {
// Все остальные маски попадают в этот блок
        byte subnet[4];
        parseBytes(input.c_str(), ‘.’, subnet, 4, 10);
config.subnet = {subnet[0], subnet[1], subnet[2], subnet[3]};
    }
}

MAC адрес хранится у нас в виде массива байт. Его перезапись другим массивом производится с помощью функции memcpy

if (inputCommands.startsWith(F(“mac”))) {
byte mac[6];
parseBytes(inputCommands.substring(4).c_str(), ‘:’, mac, 6, 16);
memcpy(config.mac, mac, 6);
}

Изменение адреса MQTT сервера

if (inputCommands.startsWith(F(“server”))) {
String server = inputCommands.substring(8);
server.trim();
if (server.length() < 40) server.toCharArray(config.server, 40);
}

В принципе теперь понятно, как производить получение, разбор и сохранение конфигурации в EEPROM микроконтроллера.

Как это выглядит на практике

Заливаем программу в микроконтроллер и подключаемся к Arduino по usb или через переходник. Открываем терминал и нас приветствуют краткой справкой с описанием доступных команд.

– —————————————————————————————
# Sensor with data sending to mqtt server (c) nfcexpert.ru
# Use the “config” command to view the current configuration
# To change the configuration, specify the parameter name and its new value with a space,
# for example “ip 192.168.0.200”, “subnet 255.255.255.0” or “mac AA:BB:CC:DD:EE:FF”
# You can also specify a subnet using the mask 24, 16 or 8
# Additional commands:
# sensors – view current data from sensors
# config – view current configuration
# save – saves the current configuration
# reset – resets all settings
# restart – restarts the device
# eeprom clear – removes all contents of eeprom
# help – view this help
– —————————————————————————————

Т.к. в EEPROM микроконтроллера не была обнаружена конфигурация (волшебный hex байт нам подсказал), то были задействованы стандартные настройки. Просмотреть текущую конфигурацию можно командой config

config
# ip: 192.168.0.200
# subnet: 255.255.255.0
# gateway: 192.168.0.1
# dns: 192.168.0.1
# mac: 00:AA:BB:CC:DE:02
# server: mqtt.nfcexpert.ru
# topic: arduino/serial/config

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

ip 10.10.10.99
# ok
gateway 10.10.10.1
# ok
dns 10.10.10.1
# ok

После окончания конфигурации необходимо сохранить настройки и если были затронуты критические параметры, например, сеть, то перезапустить Arduino соответствующей командой.

save
# ok
restart
# ok
# restarting device…

Если параметр был успешно принят, то контроллер ответит нам “ok”, а в противном случае ругнется.

ip 127.0.0.1
# bad ip

Также мы получим негативный ответ если команда не была распознана.

qwerqwer1243
# bad command

С остальными командами Вы разберетесь самостоятельно.

Исходник: MQTT_CLIENT_328_SERIAL_CONFIG.zip

PS: в общем то это статья родилась только для того, чтобы в соседнем форуме с системой мониторинга Zabbix появилась ссылка на устройство, но я надеюсь, что она также станет полезна любителям домашней автоматизации и не только.

Изображения плат ардуино уно

Оригинальная плата выглядит следующим образом:

Оригинальный Arduino Uno
Оригинальный и официальный Arduino Uno

Многочисленные китайские варианты выглядят вот так:

Клон Arduino Uno
Плата – клон Arduino Uno

Еще примеры плат:

Плата Arduino Uno R3: схема, описание, подключение устройств

Об rfid-метках и работу с ними при помощи arduino | блог интернет-магазина nfcexpert.ru

Вам, конечно же, знакомы карточки и брелки, которые нужно подносить к считывателю, а они при этом пропускают на работу или дают проехать в метро. Такие брелки и карточки используют технологию под названием RFID — Radio Frequency IDentification. Сегодня мы познакомимся с основами этой технологии, а также узнаем, как использовать ее в своих проектах на базе Arduino.

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

Стоит отметить, что это описание касалось пассивных меток. Бывают и активные метки, имеющие собственный источник питания, а также полупассивные. Что же касается радиосигнала, в RFID сигнал типично передается на частоте 125 КГц или 13.56 МГц. Существует множество стандартов передачи сигнала и их реализаций. Довольно распространенным является стандарт ISO/IEC 14443 и его реализация MIFARE от компании NXP Semiconductors. Еще одним известным стандартом является NFC, основанный на ISO 14443. Одна из его реализаций называется NTAG, также от NXP Semiconductors. Стоит отметить, что в общем случае реализации одного стандарта от разных производителей могут быть не вполне совместимы друг с другом и содержать расширения, которых нет в самом стандарте.

Fun fact! В метках MIFARE используется секретный шифр Crypto-1, выжигаемый прямо в железе, что изначально делало невозможным клонирование этих меток. Шифр со временем отреверсили и предали широкой огласке, поэтому последние лет 10 метки MIFARE может свободно клонировать кто угодно. Кое-какие подробности описаны в этих слайдах. Эта история наглядно демонстрирует, почему безопасность через неясность (security by obscurity) не работает.

RFID-модуль RC522 для Arduino
Комплект для Arduino — RFID модуль RC522, карта, брелок, штырьковые разъемы

Для Arduino существует несколько модулей для работы с RFID. Пожалуй, самым дешевым и в то же время самым популярным, является модуль под названием RC522 на базе чипа MFRC522 от NXP. Чип MFRC522 поддерживает технологии MIFARE и NTAG, радиосигнал передается на частоте 13.56 МГц. С микроконтроллером чип общается по SPI. Однако прямо по SPI ходить в чип нам не придется, так как для работы с модулем существует готовая библиотека MFRC522. Ее можно установить прямо из Arduino IDE.

Подключение модуля к Arduino Uno осуществляется так:

  • Пины 3.3V и GND Arduino подключаем к аналогичным пинам модуля;
  • Пины с 9 по 13 Arduino подключаем к пинам RST, SDA, MOSI, MISO и SCK модуля соответственно;

В итоге должно получиться что-то в таком стиле:

RFID-модуль для Arduino

Рассмотрим простейший пример кода:

Этот код полностью считывает содержимое метки и передает его в компьютер по UART. Если теперь поднести карточку к считывателю, вы увидите что-то вроде:

Отсюда мы можем узнать, что метка имеет 32-х битный идентификатор (UID), а также содержит в себе 1 Кб данных. Также можно заметить, что UID на самом деле представляет собой данные, хранящиеся в нулевом блоке.

Теперь допустим, что у нас есть дверь, и мы хотим открывать ее только тем, кто приложит правильную карточку. Проще всего это сделать, проверяя UID карточки:

Вместо UID можно с тем же успехом использовать другие блоки. Как вариант, в них можно хранить битовую маску, определяющую, какие двери можно открывать с помощью этой карты, а какие нельзя. Чтобы нельзя было так просто изготовить мастер-ключ, открывающий все двери, данные на карте можно подписывать, например, при помощи эллиптической криптографии.

Как видите, пользоваться модулем достаточно просто. С его помощью вы без проблем добавите в ваши проекты аутентификацию по карточкам. Стоит только иметь ввиду, что MIFARE-метки достаточно легко клонируются. Вместе с библиотекой MFRC522 идет еще масса примеров, включая смену UID и полное клонирование карт.

Оригинал статьи в блоге «Записки программиста».

Купить RFID RC522 13.56MHz в интернет-магазине nfcexpert.ru

Купить набор Arduino c модулем RFID RC522 13.56MHz

Перевод данных для передачи

Тут тоже следует освежить в памяти форматы данных, хранимые на карте. То, в каком виде они записаны. Давайте на живом примере.

Предположим у нас есть карта, но нет ридера. На карте написан номер 010,48351.

Реальная карта с номером 010, 48351.

Как этот номер нам перевести в тот серийный номер, который записан на карте? Достаточно просто. Вспоминаем формулу: переводим две части числа отдельно:

010d = 0xA
48351d = 0xBCDF

Итого, серийный номер у нас получается: 0xABCDF. Проверим его, считываем карточку считывателем (он читает в десятичном формате), получаем число:

0000703711

Переводим его любым калькулятором в хекс-формат и получаем снова: 0xABCDF.


Вроде пока просто, погодите, сейчас мозги придётся поднапрячь. Напомню формат данных, которые лежат на самой карте.

Проговорю словами:

  1. Вначале идут девять единиц заголовка.
  2. Младшие пол байта ID клиента.
  3. В конце бит чётности.
  4. Вторые пол байта ID клиента.
  5. Бит чётности.
  6. Младшие пол байта нулевого байта серийного номера.
  7. Бит чётности
  8. Старшие пол байта данных байта нулевого байта серийного номера.
  9. Точно так же все остальные данные, передаются ниблами и оканчиваются битом чётности
  10. Самое сложное. Теперь все эти 10 нибблов по вертикали точно так же вычисляется бит чётности (прямо как в таблице).
  11. Завершает всё это безобразие стоп бит, который равен всегда нулю.

Итого у нас получается 64 бита данных (это из пяти байт!). В качестве ремарки, мой считыватель не читает ID-клиента, и я его принимаю равным нулю.

Читайте ещё про NFC:  Подробный обзор Android Pay: где работает, как защищён, как начать пользоваться

Что такое бит чётности? Это количество единиц в посылке: если оно чётное, то бит чётности равен нулю, если нет, то единице. Проще всего рассчитать его, просто обычным XOR.

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

Тестовая программа перевода серийника в данные

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c"
#define BYTE_TO_BINARY(byte)  
  (byte & 0x80 ? '1' : '0'), 
  (byte & 0x40 ? '1' : '0'), 
  (byte & 0x20 ? '1' : '0'), 
  (byte & 0x10 ? '1' : '0'), 
  (byte & 0x08 ? '1' : '0'), 
  (byte & 0x04 ? '1' : '0'), 
  (byte & 0x02 ? '1' : '0'), 
  (byte & 0x01 ? '1' : '0') 

#define NYBBLE_TO_BINARY_PATTERN "%c%c%c%c"
#define NYBBLE_TO_BINARY(byte)  
	(byte & 0x08 ? '1' : '0'), 
	(byte & 0x04 ? '1' : '0'), 
	(byte & 0x02 ? '1' : '0'), 
	(byte & 0x01 ? '1' : '0') 


int main() {
	//unsigned long long card_id = 0x00000ABCDF;
	//uint64_t card_id = 0x00000ABCDF;
	uint64_t card_id = (uint64_t)3604000;
	uint64_t data_card_ul = 0x1FFF; //first 9 bit as 1
	int32_t i;
	uint8_t tmp_nybble;
	uint8_t column_parity_bits = 0;
	printf("card_id = 0x%lXn", card_id);
	for (i = 9; i >= 0; i--) { //5 bytes = 10 nybbles
		tmp_nybble = (uint8_t) (0x0f & (card_id >> i*4));
		data_card_ul = (data_card_ul << 4) | tmp_nybble;
		printf("0xX", (int) tmp_nybble);
		printf("t"NYBBLE_TO_BINARY_PATTERN, NYBBLE_TO_BINARY(tmp_nybble));
		printf("t %dn", (tmp_nybble >> 3 & 0x01) ^ (tmp_nybble >> 2 & 0x01) ^
			(tmp_nybble >> 1 & 0x01) ^ (tmp_nybble  & 0x01));
		data_card_ul = (data_card_ul << 1) | ((tmp_nybble >> 3 & 0x01) ^ (tmp_nybble >> 2 & 0x01) ^
			(tmp_nybble >> 1 & 0x01) ^ (tmp_nybble  & 0x01));
		column_parity_bits ^= tmp_nybble;
	}
	data_card_ul = (data_card_ul << 4) | column_parity_bits;
	data_card_ul = (data_card_ul << 1); //1 stop bit = 0
	printf("t"NYBBLE_TO_BINARY_PATTERN"n", NYBBLE_TO_BINARY(column_parity_bits));
	printf("data_card_ul = 0x%lXn", data_card_ul);
	
	for (i = 7; i >= 0; i--) {
		printf("0xX,", (int) (0xFF & (data_card_ul >> i * 8)));
	}
	printf("n");
	return 0;
}

Самое важное для нас, это то как будет выглядеть биты чётности. Для удобства я сделал вывод на экран точно так же, как в этой табличке. В результате получилось вот так.

card_id

— это серийный номер карты (о котором мы говорили выше).

Первый столбец — это ниблы, второй — их битовое представление, третий — это бит чётности. Третья строка снизу — это биты чётности всех ниблов. Как я уже сказал, они рассчитываются просто операцией XOR.

Протестировав расчёты, сверив иx визуально, я проверил получившиеся данные в программе на Arduino (последняя строка специально для вставки в код). Всё отработало отлично. В результате наброска этой программы, я получил готовую функцию пересчёта. Раньше, расчёты битов были чужими программами на компе и мне не нравилась их монструозная реализация. Таким образом функция пересчёта серийного номера в формат передачи выглядит так:


#define CARD_ID 0xABCDF

uint8_t data[8];

void data_card_ul() {
  uint64_t card_id = (uint64_t)CARD_ID;
  uint64_t data_card_ul = (uint64_t)0x1FFF; //first 9 bit as 1
  int32_t i;
  uint8_t tmp_nybble;
  uint8_t column_parity_bits = 0;
  for (i = 9; i >= 0; i--) { //5 bytes = 10 nybbles
    tmp_nybble = (uint8_t) (0x0f & (card_id >> i*4));
    data_card_ul = (data_card_ul << 4) | tmp_nybble;
    data_card_ul = (data_card_ul << 1) | ((tmp_nybble >> 3 & 0x01) ^ (tmp_nybble >> 2 & 0x01) ^
      (tmp_nybble >> 1 & 0x01) ^ (tmp_nybble  & 0x01));
    column_parity_bits ^= tmp_nybble;
  }
  data_card_ul = (data_card_ul << 4) | column_parity_bits;
  data_card_ul = (data_card_ul << 1); //1 stop bit = 0
  for (i = 0; i < 8; i  ) {
    data[i] = (uint8_t)(0xFF & (data_card_ul >> (7 - i) * 8));
  }
}

Всё, можно переходить к полевым испытаниям. Исходный код проекта обитает

Шаг 5: код проекта

Чтобы код проекта был скомпилирован, нам нужно включить некоторые библиотеки. Прежде всего, нам нужна библиотека MFRC522 Rfid.

Чтобы установить её, перейдите в Sketch -> Include Libraries -> Manage libraries (Управление библиотеками). Найдите MFRC522 и установите её.

Нам также нужна библиотека Adafruit SSD1306 и библиотека Adafruit GFX для отображения.

Установите обе библиотеки. Библиотека Adafruit SSD1306 нуждается в небольшой модификации. Перейдите в папку Arduino -> Libraries, откройте папку Adafruit SSD1306 и отредактируйте библиотеку Adafruit_SSD1306.h. Закомментируйте строку 70 и раскомментируйте строку 69, т.к. дисплей имеет разрешение 128×64.

Сначала мы объявляем значение метки RFID, которую должен распознать Arduino. Это массив целых чисел:

int code[] = {69,141,8,136}; // UID

Затем мы инициализируем считыватель RFID и дисплей:

rfid.PCD_Init();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

После этого в функции цикла мы проверяем тег на считывателе каждые 100 мс.

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

if(match)
    {
      Serial.println("nI know this card!");
      printUnlockMessage();
    }else
    {
      Serial.println("nUnknown Card");
    }

Конечно, вы можете изменить этот код, чтобы сохранить более 1 значения UID, чтобы проект распознал больше RFID-меток. Это просто пример.

Код проекта:

#include <MFRC522.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SPI.h>


#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

#define SS_PIN 10
#define RST_PIN 9
 
MFRC522 rfid(SS_PIN, RST_PIN); // Instance of the class

MFRC522::MIFARE_Key key; 

int code[] = {69,141,8,136}; //This is the stored UID
int codeRead = 0;
String uidString;
void setup() {
  
  Serial.begin(9600);
  SPI.begin(); // Init SPI bus
  rfid.PCD_Init(); // Init MFRC522 
  
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3D (for the 128x64)

  // Clear the buffer.
  display.clearDisplay();
  display.display();
  display.setTextColor(WHITE); // or BLACK);
  display.setTextSize(2);
  display.setCursor(10,0); 
  display.print("RFID Lock");
  display.display();
  
}

void loop() {
  if(  rfid.PICC_IsNewCardPresent())
  {
      readRFID();
  }
  delay(100);

}

void readRFID()
{
  
  rfid.PICC_ReadCardSerial();
  Serial.print(F("nPICC type: "));
  MFRC522::PICC_Type piccType = rfid.PICC_GetType(rfid.uid.sak);
  Serial.println(rfid.PICC_GetTypeName(piccType));

  // Check is the PICC of Classic MIFARE type
  if (piccType != MFRC522::PICC_TYPE_MIFARE_MINI &&  
    piccType != MFRC522::PICC_TYPE_MIFARE_1K &&
    piccType != MFRC522::PICC_TYPE_MIFARE_4K) {
    Serial.println(F("Your tag is not of type MIFARE Classic."));
    return;
  }

    clearUID();
   
    Serial.println("Scanned PICC's UID:");
    printDec(rfid.uid.uidByte, rfid.uid.size);

    uidString = String(rfid.uid.uidByte[0]) " " String(rfid.uid.uidByte[1]) " " String(rfid.uid.uidByte[2])  " " String(rfid.uid.uidByte[3]);
    
    printUID();

    int i = 0;
    boolean match = true;
    while(i<rfid.uid.size)
    {
      if(!(rfid.uid.uidByte[i] == code[i]))
      {
           match = false;
      }
      i  ;
    }

    if(match)
    {
      Serial.println("nI know this card!");
      printUnlockMessage();
    }else
    {
      Serial.println("nUnknown Card");
    }


    // Halt PICC
  rfid.PICC_HaltA();

  // Stop encryption on PCD
  rfid.PCD_StopCrypto1();
}

void printDec(byte *buffer, byte bufferSize) {
  for (byte i = 0; i < bufferSize; i  ) {
    Serial.print(buffer[i] < 0x10 ? " 0" : " ");
    Serial.print(buffer[i], DEC);
  }
}

  void clearUID()
  {
    display.setTextColor(BLACK); // or BLACK);
    display.setTextSize(1);
    display.setCursor(30,20); 
    display.print(uidString);
    display.display();
  }

  void printUID()
  {
    display.setTextColor(WHITE); // or BLACK);
    display.setTextSize(1);
    display.setCursor(0,20); 
    display.print("UID: ");
    display.setCursor(30,20); 
    display.print(uidString);
    display.display();
  }

  void printUnlockMessage()
  {
    display.display();
    display.setTextColor(BLACK); // or BLACK);
    display.setTextSize(2);
    display.setCursor(10,0); 
    display.print("RFID Lock");
    display.display();
    
    display.setTextColor(WHITE); // or BLACK);
    display.setTextSize(2);
    display.setCursor(10,0); 
    display.print("Unlocked");
    display.display();
    
    delay(2000);
    
    display.setTextColor(BLACK); // or BLACK);
    display.setTextSize(2);
    display.setCursor(10,0); 
    display.print("Unlocked");

    display.setTextColor(WHITE); // or BLACK);
    display.setTextSize(2);
    display.setCursor(10,0); 
    display.print("RFID Lock");
    display.display();
  }

Считывание карты и вывод номера в монитор последовательного порта.

Подключим модуль по шине I2C.

#include <Wire.h>                                                                               // подключаем библиотеку для работы по I2C
#include <Adafruit_PN532.h>                                                                     // подключаем библиотеку для работы с модулем
#define PN532_IRQ   (2)                                                                         // указываем вывод, к которому подключен контакт IRQ
#define PN532_RESET (3)                                                                         // указываем вывод, к которому подключен контакт RST
Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);                                                     // назначаем имя модулю

void setup() {
  Serial.begin(115200);                                                                         // инициируем передачу данных в монитор последовательного порта на скорости 115200 бод
  Serial.println("Hello!");                                                                     // выводим в монитор порта текст
  nfc.begin();                                                                                  // инициируем работу с модулем
  uint32_t versiondata = nfc.getFirmwareVersion();                                              // считываем версию прошивки модуля в переменную
  if (! versiondata) {                                                                          // если переменная пуста, то
    Serial.print("Didn't find PN53x board");                                                    // выводим текст в монитор порта
    while (1);                                                                                  // прерываем дальнейшее выполнение блока setup
  }
  Serial.print("Found chip PN5"); Serial.println((versiondata >> 24) & 0xFF, HEX);              // если версия прочитана, то выводим текст и версию чипа в монитор порта
  Serial.print("Firmware ver. "); Serial.print((versiondata >> 16) & 0xFF, DEC);                // выводим текст и версию прошивки до запятой в монитор порта
  Serial.print(','); Serial.println((versiondata >> 8) & 0xFF, DEC);                            // выводим запятую и версию прошивки после запятой
  nfc.setPassiveActivationRetries(0xFF);                                                        // указываем количество попыток на считывание карты
  nfc.SAMConfig();                                                                              // настраиваем модуль на чтение RFID-меток
  Serial.println("Waiting for an ISO14443A card");                                              // выводим текст в монитор порта
}
void loop() {
  boolean success;                                                                              // задаём переменную для считывания номера карты
  uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 };                                                      // задаём переменную для хранения номера считанной карты
  uint8_t uidLength;                                                                            // длина номера карты (4 или 7 байт в зависимости от типа карты)
  success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, &uid[0], &uidLength);               // считываем первую карту для определения размера и назначения типа карты
  if (success) {                                                                                // если номер считан, то
    Serial.println("Found a card!");                                                            // выводим текст в монитор порта о том, что карта найдена
    Serial.print("UID Length: "); Serial.print(uidLength, DEC); Serial.println(" bytes");       // выводим текст и значение длины карты в байтах
    Serial.print("UID Value: ");                                                                // выводим текст в монитор порта
    for (uint8_t i = 0; i < uidLength; i  ) {                                                   // выводим номер карты (в зависимости от длины номера будет меняться и количество раз, которые должен выполнить цикл)
      Serial.print(" 0x"); Serial.print(uid[i], HEX);                                           // выводим текст и побитовый номер карты
    }
    Serial.println("");                                                                         // добавляем символ новой строки
    delay(1000);                                                                                // ждём 1 секунду
  }
  else  {                                                                                       // если карта была не прочитана, то
    Serial.println("Timed out waiting for a card");                                             // выводим текст в монитор порта о том, что время ожидания карты истекло
  }
}

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

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

Adblock
detector