Что нужно
- Модуль отпечатков пальцев FPM10A
- RFID-модуль RC522
- Arduino mega (теоретически можно использовать и другие платы но я остановился на данной из-за количества выводов)
- 1 лицензия на 1С 8.2 (толстый клиент в моём случае, под тонкий пилите сами)
- Связь по Com порту с помощью MsCommLib.MsComm (нужна лицензия, ягуглится даже тут)
- Среда разработки для ардуино (использована 1.8.5)
- Планшет с USB интерфейсом.
- Обвязка: Резистор 1 Кило ОМ, кнопка на замыкание, проводочки, "синяя" изолента, терморектальный крипоанализатор по желанию (можно скрутить проводочки если с данным прибором вас связывают тесные непередаваемые ощущения), USB кабель для ардуиино, прямые руки и кривые извилины среднестатистического 1с ника в т.ч. для изготовления корпуса для всего этого чуда.
Примечания: на фото цвета проводов разнятся - в процессе монтажа порвал несколько проводов т.к. они были совсем Huawei и пришлось брать других доступных цветов.
Система сообщений дублируется в критических моментах специально.
Сразу предупреждаю что может у среднестатистического инженера могут потечь слезы из глаз из канифоли но это мой вообще первый Ардуино проект да еще и такой сложности и вроде оно работает и работает стабильно. Сначала была идея сделать всё через сеть (wifi + проводная) но прикинув затраты на отладку и создание своего http сервиса и внедрение всё в 1с я решил использовать com, в любом случае всю логику можно без изменения конфы вынести во внешнюю обработку.
Можно ещё вставить систему фотографирования входящего сотрудника через web камеру планшета, добавить реле и управлять электронными воротами, допилить интеграцию с ЗУП через внешнюю обработку, передаваемую через параметры запуска и аннигиляцию масленницы для особо опасных проходных секретной важности ).
Полезная критика приветствуется.
Пролог
Видя цены на существующие пропусные системы и системы учета времени меня медленно начала душить зеленая сущность. Долго ходив около ардуинки и её модулей наткнулся на модуль отпечатков пальцев FPM10A. Данный модуль в зависимости от версии может запоминать большое количество отпечатков пальцев - от 50 до бесконечности её флеш памяти и используется в большинстве модулей производителей биометрического контроля. Однако в моём проекте из-за библиотеки оно ограниченно 254. Сразу предупреждаю что выкладываю свою измученно-найденную библиотеку для ардуино т.к. долго мучался с поиском и угробил 3 дня на поиски и отладку библиотеки для данного модуля.
Описание модулей
Библиотека, использованная в проекте позволяет использовать до 256 (byte) отпечатков пальцев. Мне это количество было избыточным, в крайнем случае можно использовать по 1 модулю на каждых 256 сотрудников.
Количество RFID меток ограниченно лишь уникальностью их UID т.к. база может хранится в 1С и привязанна к сотрудникам. Метки можно использовать все совместимые. Теоретически могут подходить всякие ключи от домофонов, карты метро и карты тройка.
Соединение с 1с идёт по com порту через библиотеку MsCommLib.MsComm но можно переписать под любую другую. Драйвера для com порта для Ардуинки должны ставится вместе со средой разработки ардуино но так же ягуглятся.
Всё спаенное прячется в коробку, связывается по сети (я использовал WiFi но можно и внешнюю USB сетевую карту использовать).
Алгоритм работы
Аппаратно:
- Модули ардуино спаиваем/скручиваем
- Подключаем к ПК-программатору и заливаем прошивку в арду, запускаем тест, убеждаемся в работоспособности команд
- Через USB соединяются с планшетом на Windows 10. На планшете заменяем либо через автозагрузку:
А) через батник:
-
пуск - выполнить: shell:startup
-
создаем там через "блокнот" файл с именем hz.bat и содержанием (уверен что сами справитесь с параметрами файловой базы - у меня sql): "C:\Program Files\1cv8\ ... \bin\1cv8.exe" ENTERPRISE /SServerName:Port\DBName" /NUser /PPassword
Б) более совершенный через замену шелла делаем через VB script (обязательно создайте кроме пользователя по умолчанию другого без запуска шелла):
-
создаем через "блокнот" файл с именем C:\hz\hz.vbs и содержанием
set oShell=createobject("wscript.shell")
sCmd="""C:\Program Files\1cv8\ ... \bin\1cv8.exe"" ENTERPRISE /SServerName:Port\DBName" /NUser /PPassword"
oShell.run sCmd,,true
sCmd="shutdown /r /t 0"
oShell.run sCmd
-
пуск - выполнить: regedit, идём по ветке: Current User\Software\Microsoft\Windows NT\CurrentVersion\Winlogon
-
добавляем строковый параметр "Shell" (REG_SZ)
-
редактируем его: "wscript C:\hz\hz.vbs" (без кавычек в параметре)
-
ребут и тест. 1с должна запускаться без explorera
- Далее убеждаемся в работоспособности и упаковываем в коробку
Программно:
- В обычном состоянии ардуино опрашивает сканер отпечатков пальцев, RFID сканер, кнопку администрирования и посылает по com порту команды ожидания.
- Как только в поле зрения считывателей появляется палец посылаем команду в com порт и 1с видит либо ID пальца, либо UID метки через чтение переменных.
- Кнопка нужна для администирования отпечатков. При её нажатии 1с опрашивает пароль-логин и дальше может присвоить сотруднику либо ID сканера либо UID карты через систему сообщений.
Для связи с 1с используется следующие строки (обработку я делал для своей конфы и своего табеля, она в проекте просто для примера но включена в исходники):
Подключение к com порту
Процедура СтартСистемы()
ComPort = Новый COMОбъект("MsCommLib.MsComm");
Попытка
ComPort.CommPort = 3;
ComPort.Settings = "9600,N,8,1";
ComPort.Handshaking = 0;
ComPort.InBufferCount = 0;
ComPort.InBufferSize = 70;
ComPort.InputLen = 0;
ComPort.InputMode = 1;
ComPort.NullDiscard = 0;
ComPort.PortOpen = Истина;
Исключение
Предупреждение("Не возможно открыть порт!");
ЭтаФорма.Закрыть();
КонецПопытки;
чСекунд = 0;
ПодключитьОбработчикОжидания("ВывестиДанныеСПорта", 1, Ложь); // Подключим обработчик для мониторинга порта
КонецПроцедуры
Отключение от com порта
Процедура КонецСистемы()
ОтключитьОбработчикОжидания("ВывестиДанныеСПорта");
ComPort.PortOpen = Ложь;
ComPort = "";
КонецПроцедуры
Считывание данных с com порта
Процедура ВывестиДанныеСПорта() Экспорт
ДанныеСПорта = "";
Если ComPort.PortOpen Тогда
//ComPort.Output = "1";
ДанныеСПорта = ComPort.Input;
ОбработатьЗашифрованнуюСтроку(ДанныеСПорта);
Если СокрЛП(ПредСотрудник) <> "" Тогда
чСекунд = чСекунд + 1;
КонецЕсли;
Если чСекунд > 60 Тогда
ПредСотрудник = 0;
чСекунд = 0;
КонецЕсли;
Иначе
Предупреждение("Порт не открывается");
ЭтаФорма.Закрыть();
КонецЕсли;
КонецПроцедуры
Процедура ОбработатьЗашифрованнуюСтроку(ДанныеСПорта)
Массив = ДанныеСПорта.Выгрузить();
ИндексМин = ДанныеСПорта.GetLowerBound(0);
ИндексМакс = ДанныеСПорта.GetUpperBound(0);
СтрокаИнфо = "";
Для Индекс = ИндексМин По ИндексМакс - 1 Цикл
СимволПолученный = СокрЛП(Массив.Получить(Индекс));
Если СимволПолученный = "13" Тогда
Если Не Приостановить Тогда
СтрокаИнфо = ОбработкаСтроки(СтрокаИнфо); //Тут обработка сообщений
КонецЕсли;
Иначе
СтрокаИнфо = СтрокаИнфо + Символ(Число(СимволПолученный));
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Отправка информации в com порт
Процедура ОтправитьВПорт(Отправить)
Если ComPort.PortOpen Тогда
ComPort.Output = СокрЛП(Отправить);
Иначе
Сообщить("Порт не открывается",СтатусСообщения.ОченьВажное);
КонецЕсли;
КонецПроцедуры
Код проекта для Arduino Mega
#include <Adafruit_Fingerprint.h> // подключаем библиотеку для работы с модулем отпечатков пальцев
#include <SoftwareSerial.h> // подключаем библиотеку для работы с программным UART
#include <SPI.h>
#include <MFRC522.h>
const int buttonPin = 2; // номер входа, подключенный к кнопке для входа в режим программирования
int buttonState = 0; // переменная для хранения состояния кнопки
int modeState = 0; // переменная для хранения состояния устройства. 0 - ожидание сканера отпечатков. 1 - программирование
uint8_t id; // идентификационный номер, под которым будет сохранён шаблон отпечатка пальца
String frcUID = ""; // идентификационный номер считывателя rfid
int rfidYes = 0; // успешный ввод RFID
SoftwareSerial mySerial(10, 11); // объявляем объект mySerial для работы с библиотекой SoftwareSerial ИМЯ_ОБЪЕКТА( RX, TX ); // Можно указывать любые выводы, поддерживающие прерывание PCINTx
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&mySerial); // объявляем объект finger для работы с библиотекой Adafruit_Fingerprint ИМЯ_ОБЪЕКТА = Adafruit_Fingerprint(ПАРАМЕТР); // ПАРАМЕТР - ссылка на объект для работы с UART к которому подключен модуль, например: &Serial1
MFRC522 mfrc522(53, 5); // Create MFRC522 instance
void setup() {
pinMode(buttonPin, INPUT); // инициализируем пин, подключенный к кнопке, как вход
Serial.begin(9600); // Инициализация аппаратного UART на скорости 9600
while (!Serial); // Ожидание инициализации аппаратного UART
delay(500);
SPI.begin(); // Init SPI bus
mfrc522.PCD_Init(); // Init MFRC522
mfrc522.PCD_DumpVersionToSerial(); // Show details of PCD - MFRC522 Card Reader details
delay(500); // Ожидание инициализации модуля отпечатков пальцев
Serial.println(". . . Scan sensor . . ."); // Вывод сообщения "Поиск сенсора"
finger.begin(57600); // Инициализация программного UART на скорости 57600 (скорость модуля по умолчанию)
Serial.println(finger.verifyPassword());
if (finger.verifyPassword()) {
Serial.println(". . . Found sensor! . . ."); // Если модуль отпечатков обнаружен, выводим сообщение "сенсор обнаружен"
}
else {
Serial.println(". . . Did not find sensor . . ."); // Если модуль отпечатков не обнаружен, выводим сообщение "сенсор не обнаружен" и входим в бесконечный цикл: while(1);
while (1);
}
Serial.println(". . . Please put your finger on the scanner or rfid . . .");
}
void loop() {
//Работа с кнопкой!
buttonState = digitalRead(buttonPin); // считываем значения с входа кнопки
if (buttonState == HIGH) {
modeState = 1; // входим в режим программирования ДАЛЕЕ
}
switch (modeState) {
case 0:
frcUID = "";
//Работа в режиме опроса отпечатков пальцев и rfid
if (finger.getImage() == FINGERPRINT_OK) { // Захватываем изображение, если результат выполнения равен константе FINGERPRINT_OK (корректная загрузка изображения), то проходим дальше
if (finger.image2Tz() == FINGERPRINT_OK) { // Конвертируем полученное изображение, если результат выполнения равен константе FINGERPRINT_OK (изображение сконвертировано), то проходим дальше
if (finger.fingerFastSearch() == FINGERPRINT_OK) { // Находим соответствие в базе данных отпечатков пальцев, если результат выполнения равен константе FINGERPRINT_OK (найдено соответствие), то проходим дальше
frcUID = ". . . Found ID=" + String(finger.fingerID) + ", confidence=" + String(finger.confidence) + "! . . .";
Serial.println(frcUID);
}
}
}
if (mfrc522.PICC_IsNewCardPresent()) {
delay(100);
if (mfrc522.PICC_ReadCardSerial()) {
//mfrc522.PICC_DumpToSerial(&(mfrc522.uid));
frcUID = "";
for (byte i = 0; i < mfrc522.uid.size; i++) {
frcUID = frcUID + (mfrc522.uid.uidByte[i]);
}
frcUID = ". . . Found RFID UID=" + frcUID + "@ . . .";
Serial.println(frcUID);
}
}
delay(100); // Задержка перед следующим сканированием 0,5 сек (нет смысла запускать на полной скорости)
Serial.println(". . . Please put your finger on the scanner or rfid . . .");
break;
case 1:
Serial.println(". . . Programming mode . . ."); // Входим в режим программирования
delay(400);
Serial.println(". . . Programming mode . . ."); // Входим в режим программирования
delay(400);
Serial.println(". . . Programming mode . . ."); // Входим в режим программирования
delay(400);
Serial.println(". . . Programming mode . . ."); // Входим в режим программирования
id = readnumber(); // Ожидание получения цифры, введённой с COM-порта
if (id >= 255) { // Если 255 (макс ид) то входим в режим ожидания снова
modeState = 0;
} else {
if (id < 254) { // Если 254 то rfid иначе палец 0-253
modeState = 2; // Пытаемся отсканировать палец
}
else {
modeState = 3; // Пытаемся отсканировать rfid uid
}
}
break;
case 2:
while (!getFingerprintEnroll()); // Пытаемся получить ответ об присваивании ID
modeState = 1;
break;
case 3:
rfidYes = 0;
Serial.println(". . . Put RFID in Scanner! . . .");
delay(400);
Serial.println(". . . Put RFID in Scanner! . . .");
delay(400);
Serial.println(". . . Put RFID in Scanner! . . .");
delay(5000);
if (mfrc522.PICC_IsNewCardPresent()) { // Пытаемся отсканировать rfid uid
delay(100);
if (mfrc522.PICC_ReadCardSerial()) {
//mfrc522.PICC_DumpToSerial(&(mfrc522.uid));
frcUID = "";
for (byte i = 0; i < mfrc522.uid.size; i++) {
frcUID = frcUID + (mfrc522.uid.uidByte[i]);
}
frcUID = ". . . New RFID UID=" + frcUID + "@ . . .";
rfidYes = 1;
Serial.println(frcUID);
delay(400);
Serial.println(frcUID);
delay(400);
Serial.println(frcUID);
delay(400);
Serial.println(frcUID);
}
}
if (rfidYes == 0) {
Serial.println(". . . RFID error! . . .");
delay(400);
}
modeState = 1;
break;
}
}
// функция возвращает номер, введённый с COM-порта
uint8_t readnumber(void) {
int num = -1; // Переменная с номером, который требуется вернуть
while (num < 0) { // Вход в цикл, пока переменная num не станет >= 0
while (!Serial.available()); // Ожидание пока в буфере COM-порта нет появятся данные
while (Serial.available()) { // Цикл пока в буфере COM-порта не закончатся данные
char c = Serial.read(); // Присваиваем очередной символ из COM-порта в переменную c
if (isdigit(c)) { // Если значение переменной с - цифра, то ...
if (num < 0) {
num = 0; // Увеличиваем значение num на один порядок
} else {
num *= 10;
}
num += c - '0'; // Прибавляем к значению num цифру из переменной c
} delay(5); // Задержка на 5мс, чтоб в буфер COM-порта успели догрузиться следующие символы (если таковые имеются)
}
}
return num; // Возвращение введённого числа
}
uint8_t getFingerprintEnroll() {
int p; // Переменная для получения результатов выполнения функций
//Загрузка первого изображения отпечатка пальца
p = -1;
Serial.println(". . . Please put your new finger on the scanner . . ."); // Вывод сообщения "Пожалуйста положите Ваш палец на сканер"
delay(400);
Serial.println(". . . Please put your new finger on the scanner . . ."); // Вывод сообщения "Пожалуйста положите Ваш палец на сканер"
delay(400);
Serial.println(". . . Please put your new finger on the scanner . . ."); // Вывод сообщения "Пожалуйста положите Ваш палец на сканер"
delay(400);
Serial.println(". . . Please put your new finger on the scanner . . ."); // Вывод сообщения "Пожалуйста положите Ваш палец на сканер"
while (p != FINGERPRINT_OK) { // Вход в цикл, пока переменная p не станет равна константе FINGERPRINT_OK (корректная загрузка изображения)
p = finger.getImage(); // Захватываем изображение и возвращаем результат выполнения данной операции в переменную p
switch (p) { // Проверка ответа ...
case FINGERPRINT_OK: Serial.println(" Ok!"); break; // Изображение отпечатка пальца корректно загрузилось
case FINGERPRINT_NOFINGER: Serial.println(". . . Please put your new finger on the scanner . . ."); break;// Сканер не обнаружил отпечаток пальца
case FINGERPRINT_PACKETRECIEVEERR: Serial.println(". . . Communication error . . ."); break; // Ошибка соединения
case FINGERPRINT_IMAGEFAIL: Serial.println(". . . Imaging error Please try again . . ."); break; // Ошибка изображения
default: Serial.println(". . . Unknown error Please try again . . ."); break; // Неизвестная ошибка
}
}
//Конвертирование изображения первого отпечатка пальца
p = finger.image2Tz(1); Serial.print (". . . Image converting . . ."); // Конвертируем первое изображение и возвращаем результат выполнения данной операции в переменную p
switch (p) { // Проверка ответа ...
case FINGERPRINT_OK: Serial.println("Ok!"); break; // Изображение сконвертировано
case FINGERPRINT_IMAGEMESS: Serial.println(". . . Image too messy . . ."); return p; // Изображение слишком нечеткое
case FINGERPRINT_PACKETRECIEVEERR: Serial.println(". . . Communication error . . ."); return p; // Ошибка соединения
case FINGERPRINT_FEATUREFAIL: Serial.println(". . . No fingerprint on image . . ."); return p; // Ошибка конвертирования
case FINGERPRINT_INVALIDIMAGE: Serial.println(". . . No fingerprint on image . . ."); return p; // Ошибка изображения
default: Serial.println(". . . Unknown error . . ."); return p; // Неизвестная ошибка
}
//Просим убрать палец от сканера
p = 0;
while (p != FINGERPRINT_NOFINGER) { // Вход в цикл, пока переменная p не станет равна константе FINGERPRINT_NOFINGER (сканер не обнаружил отпечаток пальца)
Serial.println(". . . Please remove your finger from the scanner . . ."); // Вывод сообщения "Пожалуйста уберите Ваш палец со сканера"
delay(400);
p = finger.getImage(); // Захватываем изображение и возвращаем результат выполнения данной операции в переменную p
}
Serial.println(" Ok!");
//Загрузка второго изображения отпечатка пальца
p = -1;
Serial.println(". . . Place same finger again . . ."); // Вывод сообщения "Пожалуйста положите тот же палец еще раз"
delay(400);
while (p != FINGERPRINT_OK) { // Вход в цикл, пока переменная p не станет равна константе FINGERPRINT_OK (корректная загрузка изображения)
p = finger.getImage(); // Захватываем изображение и возвращаем результат выполнения данной операции в переменную p
switch (p) { // Проверка ответа ...
case FINGERPRINT_OK: Serial.println(" Ok!"); break; // Изображение отпечатка пальца корректно загрузилось
case FINGERPRINT_NOFINGER: Serial.println(". . . Place same finger again . . ."); break; // Сканер не обнаружил отпечаток пальца
case FINGERPRINT_PACKETRECIEVEERR: Serial.println(". . . Communication error . . ."); break; // Ошибка соединения
case FINGERPRINT_IMAGEFAIL: Serial.println(". . . Imaging error . . ."); break; // Ошибка изображения
default: Serial.println(". . . Unknown error . . ."); break; // Неизвестная ошибка
}
}
//Конвертирование изображения второго отпечатка пальца
p = finger.image2Tz(2); Serial.print (". . . Image 2 converting . . ."); // Конвертируем второе изображение и возвращаем результат выполнения данной операции в переменную p
switch (p) { // Проверка ответа ...
case FINGERPRINT_OK: Serial.println("Ok!"); break; // Изображение сконвертировано
case FINGERPRINT_IMAGEMESS: Serial.println(". . . Image too messy . . ."); return p; // Изображение слишком нечеткое
case FINGERPRINT_PACKETRECIEVEERR: Serial.println(". . . Communication error . . ."); return p; // Ошибка соединения
case FINGERPRINT_FEATUREFAIL: Serial.println(". . . No fingerprint on image . . ."); return p; // Ошибка конвертирования
case FINGERPRINT_INVALIDIMAGE: Serial.println(". . . No fingerprint on image . . ."); return p; // Ошибка изображения
default: Serial.println(". . . Unknown error . . ."); return p; // Неизвестная ошибка
}
//Создание модели (шаблона) отпечатка пальца, по двум изображениям
p = finger.createModel(); Serial.print (". . . Creating model . . ."); // Создание модели (шаблона) отпечатка пальца, по двум изображениям
if (p == FINGERPRINT_OK ) {
Serial.println(". . . Model create! Ok! . . .");
} else // Модель (шаблон) отпечатка пальца создана
if (p == FINGERPRINT_PACKETRECIEVEERR) {
Serial.println(". . . Communication error . . .");
return p;
} else // Ошибка соединения
if (p == FINGERPRINT_ENROLLMISMATCH ) {
Serial.println(". . . Fingerprints did not match . . .");
return p;
} else // Отпечатки пальцев не совпадают
{
Serial.println(". . . Unknown error . . ."); // Неизвестная ошибка
return p;
}
//Сохранение, ранее созданной, модели (шаблона) отпечатка пальца, под определённым ранее, идентификационным номером
p = finger.storeModel(id);
//Serial.println(". . . Saving model . . .");
Serial.println(". . . Saving model . . .");
//Serial.println(". . . Saving model in ID="); Serial.print(id); Serial.print(": "); // Сохранение модели (шаблона) отпечатка пальца, по двум изображениям
if (p == FINGERPRINT_OK ) {
frcUID = ". . . Model save in ID=" + String(id) + "! . . .";
Serial.println(frcUID);
delay(1500);
Serial.println(frcUID);
delay(400);
Serial.println(frcUID);
delay(400);
Serial.println(frcUID);
} else // Модель (шаблон) отпечатка пальца сохранена
if (p == FINGERPRINT_PACKETRECIEVEERR) {
Serial.println(". . . Communication error . . .");
return p;
} else // Ошибка соединения
if (p == FINGERPRINT_BADLOCATION ) {
Serial.println(". . . Could not store in that location . . .");
return p;
} else // Не удалось сохранить в этом месте
if (p == FINGERPRINT_FLASHERR ) {
Serial.println(". . . Error writing to flash . . .");
return p;
} else // Ошибка записи в flash память
{
Serial.println(". . . Unknown error . . ."); // Неизвестная ошибка
return p;
}
}
Эпилог
Цена планшета около 10 000 руб. (на самом деле от 7000 новые можно купить с 1 Gb ОЗУ но с 4Gb 1с будет поприятнее двигаться, я вообще взял б.у. с рук за 6000 в идеальном состоянии с 1Gb но повозился с оптимизацией).
Проходная делалась для мебельного производства поэтому корпус я изготовил там же. Думаю что можно напилить "смесь опилок с картоном" в магазине за 1 000 руб. и скрутить саморезами/склеить суперклеем. В крайнем случае можно взять готовый ящик (например электрошкафчик) и выпилить там окно.
Модули ардуинки и прочее около 2 000 руб.
Удовольствие от мозгового штурма и изготовления - бесценно!