Буквально пару дней назад обновил свой старый замок на калитке, не планировал ничего об этом писать, но попалась статья https://habr.com/ru/news/1005908/ - "Samsung сделала цифровой ключ!"
Что ж, у меня тоже есть цифровой ключ, хоть и не Samsung.
Это не туториал, не "готовое решение", а скорее рассказ о работающей концепции, возникавших проблемах и их решении.
Началось всё давным-давно, когда мастера по установке заборов сделали ворота, калитку, и установили на нее "обычный замок", как у всех.
Обычный механический замок, из тех что ставят на гаражи, со здоровенным тяжелым ключом и минимальной"секретностью".
Сразу отмечу: секретность там особая и не нужна, основная функция замка - закрыть калитку снаружи так, чтобы ее не могли сходу открыть всякие продавцы картошки и копатели канав, но при этом ее легко могли открывать хозяева.
Так вот, неудобства типового решения проявились в первый же год: большой и очень железный ключ мешается в кармане, и если в машине его можно кинуть хотя бы в бардачок - то летом и пешком его буквально некуда деть, хоть на пояс вешай.
А зимой другая проблема: снег задувает в замочную скважину, потом оттепель, потом вечерний морозец - и замок превращается в кусок льда. Конечно, ��сть всякие размораживатели замков и прочие чудеса - но таскать с собой еще и размораживатель?!
В общем, надо было что-то делать.
Почему бы не сделать электронный замок, как у нас в офисе?
Только не вот это магнитное, которое легко можно открыть, аккуратно дернув особым образом, а нормальный замок с ригелем?
Идея получалась такая: в качестве ключа - смарткарта или что-то подобное, коды записать в микроконтроллер, и с его помощью открывать замок.
Самой сложной частью оказалось найти именно механику: тот самый замок с ригелем, который можно было дергать электричеством.
В магазинах такого не было, маркетплейсы еще не придумали, и только на известном китайском сайте можно было купить то, что нужно.
Сейчас-то это не проблема - замок-защелка с электроприводом давно не редкость, но это сейчас.
Закрывается такое устройство само, а для открытия нужно подать кратковременно 12 вольт на контакты - дверь и откроется. Как раз то, что нужно.
Следующий этап - ключ и контроллер.
Можно было просто купить готовый комплект - в какой-нибудь конторе по установке охранных сигнализаций вполне можно было заказать - но ведь так неинтересно, к тому же хотелось завязать его на системы "умного дома".
Использовать контактную "таблетку" как у подьездных домофонов не хотелось - открытые контакты, страшненькие ключи...
Поиски по тематическим ресурсам привели к ключам в виде разноцветных RFID-брелоков - они маленькие, легкие, с разными кодами, не боятся воды, снега, не цепляются ни за что и не рвут карманы.
В качестве считывателя - врезной датчик типа CP-Z: снаружи просто "кнопка", к которой надо поднести ключ.
Подобные датчики бывают разных модификаций, на разные стандарты RFID-ключей, при всём этом имеют однотипный интерфейс подключения - несколько разноцветных проводов:
красный - питание, +12В
черный - "земля"
белый и синий - DATA
Дополнительные провода могут отвечать за работу подсветки или зуммера, в моем случае это были два коричневых провода, которые отвечали за работу индикатора: светить всегда или моргать только при считывании ключа.
Всё это было написано в инструкции, которая конечно же давно утеряна.
Датчик подключается по протоколам Wiegand или iButton - в обоих случаях передается код считанного ключа. Остается его принять и обработать - и этим будет заниматься микроконтроллер.
Контроллер - на базе ESP-12F (8266), хорошо известного ардуинщикам девайса с WiFi-модулем. Правда, чаще он фигурирует под названием Wemos - тот же самый ESP-12, припаяный к плате с USB-разьемом, но зачем нам лишние детали?
ESP8266 легко программируется, и у него достаточно выводов для использования в подобных устройствах.
Программа на C++, ардуино-компилятор, заливка прошивки через TTY.
Таким образом, задача сводится к тому, чтобы подключить считыватель по протоколу iButton (эмуляция того самого "подъездного ключа-таблетки"), считать код - и если он правильный, то включить реле, которое подаст напряжение на электрозамок.
Самое очевидное решение - ключи хранятся во внутренней памяти ESP8266.
В режиме добавления новый обнаруженный ключ записывается в файл, в рабочем режиме - проверяется наличие такого ключа в этом файле, и если ключ есть - включается режим открыти�� замка.
Получился такой код (кусок кода)
... byte ctl_mode; unsigned long ctl_timer; #define CTL_ADD 2 // add mode #define CTL_OPEN 1 // opened #define CTL_CLOSE 0 // closed #define CTL_PERIOD 500 #define KEY_FILE "keys.bin" unsigned long ow_timer; #define OW_PERIOD 200 #include "OneWire.h" OneWire ds(PIN_OW); // выход считывателя byte addr[8]; void OnewireSetup(){ ow_timer = 0; } void OnewireLoop(){ char data[100]; if((millis() - ow_timer) > OW_PERIOD){ if(ds.reset()){ // если обнаружено устройттво ds.write(0x33); // отправляем команду "считать ROM" delay(50); // на всякий случай ждем/ int found=0; for(int i=0; i<8; i++){ addr[i] = ds.read(); // считываем if(addr[i] != 0xff){ found++; } } if(found == 8){ sprintf(data, "%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X", addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7]); // search at eeprom ======================= if(ctl_mode == CTL_ADD){ File file = LittleFS.open(KEY_FILE,"a+"); byte found = 0; int cnt = 0; byte buff[8]; size_t x = file.readBytes((char*)buff, 8); while(x == 8){ cnt++; if(memcmp(addr, buff, 8) == 0){ // last, found found = 1; break; } x = file.readBytes((char*)buff, 8); } // not found - new key! if(!found){ file.write(addr,8); added(); // indicate it ctl_mode = CTL_CLOSE; } file.close(); } // run mode ================================ else{ File file = LittleFS.open(KEY_FILE,"r"); byte found = 0; int cnt = 0; byte buff[8];
Логика очень простая: в случае успешного нахождения ключа устанавливается режим CTL_OPEN, это заставляет срабатывать реле, которое открывает замок.
Через небольшое время этот режим меняется на CTL_CLOSE, и реле выключается. Это позволяет избежать задержки выполнения цикла.
Также была добавлена физическая кнопка, при нажатии которой также включается реж��м CTL_OPEN, для открывания изнутри.
Тут в коде не показано, но там просто установка переменной в значение.
В коде видны функции open_lock и alarm_lock - это отправка события об открывании или попытке открытия в общую систему "умного дома": изначально модуль работал просто в режиме WiFi-клиента, подключался к MQTT-брокеру и отправлял эти события в него.
Реализация стандартная для библиотеки PubSubClient, поэтому нет смысла ее тут приводить.
Как это бывает - всё делалось на скорую руку в порядке эксперимента: модуль ESP-12, блок понижения питания с 12 до 3.3В, реле, оптопары - всё спаяно вместе в страшного "паука", засунуто в обычную пластиковую распаечную коробку с красной светящейся кнопкой, прикручено к двери.
Кнопку нажал - дверь открылась, захлопнул, ключ приложил - опять открылась.
Оказалось настолько удобно, что работало и работало, да так и осталось.
Была идея поставить вместо RFID-ридера сканер отпечатков пальцев - и такой даже был найден, но внезапно оказалось:
- грязные руки (после каких-либо работ) сводят идею к нулю: залепить грязью или моторным маслом замок легко, а оттирать его лениво и неохота.
- дождевые капли мешают
- зимой в мороз нет никакого желания снимать перчатки, и тем более отковыривать иней.
- перспектива заставлять приехавших родственников "прокатывать пальчики" совсем не радует. Да лучше им отдельных брелков раздать, каждому свой!
Поэтому отпечатки пальцев были оставлены до лучших времен.
В процессе эксплуатации выяснилось, что (внезапно) у WiFi небольшой радиус действия, связь то и дело пропадает, особенно в дождливую погоду.
Девайс пытается восстановить соединение - и в этот момент перестает обрабатывать открытие замка, причем в самый неожиданный момент.
Поскольку к тому времени это было не единственное устройство, которому было трудно достучаться до единственного роутера - подключение WiFi было заменено на PainlessMesh.
Это достаточно известная технология, по сути тот же WiFi, но тут каждый модуль работает одновременно и клиентом и точкой доступа.
Выстраивается система подключений к ближайшим соседям, что позволяет растянуть общую длину сети значительно больше, чем при обычном WiFi-соединении. При этом обмен данными идет через JSON-пакеты, подобно тому же MQTT, и в итоге они передаются в обычный MQTT-брокер.
Почти все используемые до этого модули были переведены с прямого WiFi на mesh, в том числе и замок.
По сути, менялась только та часть прошивки, которая отвечает за установление соединения, после чего модуль перепрошивается по OTA, и переходит в mesh-сеть. Та часть, которая отвечает за полезную работу, как тут за работу с ключами - остается прежней.
Теперь замок перестал терять сеть, и просто работал.
Со временем оказалось, что отсутствие тяжелого ключа может сыграть злую шутку - можно выйти и захлопнуть дверь без ключа.
Возникла задача - приделать на экране умного дома кнопку "Открыть калитку".
При нажатии кнопки формируется MQTT-команда "open", которая идет в mesh-сеть на контроллер замка, устанавливает там CTL_OPEN - дальше всё происходит само.
Так-то экран висит в доме на стене - в случае чего тому, кто остался в доме, даже не придется идти к воротам, достаточно посмотреть в камеру и нажать кнопку.
Если дома никого не осталось - это тоже не проблема:
Технически вебсервер умного дома работает на отдельном компьютере в домашней сети.
Когда-то это был старый ноутбук, теперь - одноплатник.
Доступа снаружи нет - поэтому пришлось на внешнем сервере выделить на это дело отдельный домен, при обращении на который nginx через upstream пересылает по частной приватной сети запрос прямо на этот сервер. Ну а там уже - окно авторизации, вход - и вот он, экран управления.
Делалось это не для замка, а для контроля обстановки "когда нас нет дома" - но оказалось как раз к месту.
Так проблема забытого ключа была решена - причем для этого не нужен специальный совместимый смартфон, достаточно знать куда зайти и какой пароль ввести.
Первоначальная идея о хранении ключей в контроллере работает прекрасно, но что если нужно добавить еще такой замок, на другую дверь?
Вопрос в синхронизации ключей - ведь если они храняться локально в одном контроллере, значит надо записать их в другой?
Необязательно. Ведь уже есть способ отправки события "alarm 23:43:44:22:44:66:77:22", если к считывателю поднесли неизвестный ключ, и есть способ дистанционного открытия по команде.
Оставалось сделать модуль обработки ключей: если ключ найден в БД и ему разрешен доступ - отправить на замок команду на открытие. Буквально СКУД на коленке.
Тут несколько слов о том, как всё это управление организовано:
Во-первых, всё управляется MQTT-сообщениями: температура такая-то - увеличим настройки отопления на столько-то; сработал датчик освещенности - включить свет там, там и вот там; или вот пришел ключ такой-то - отправить команду открыть.
Изначально это был скрипт, получающий и отдающий команды. Он рос, усложнялся, пока не превратился в монстра, которого надо было изучать, прежде чем вносить изменения.
И тогда было применено другое решение: какая разница, раз у нас всё равно MQTT - пусть будет 5, 10 скриптов, каждый будет заниматься своим делом, своей зоной ответственности.
Назовем их агентами (не AI! хотя...), пусть они висят, слушают команды и реагируют на них.
Один отвечает за освещение, другой следит за камерами, третий контролирует температуру.
Удобство еще и в том, что можно разнести по разным устройствам агентов, MQTT-брокер и БД, не говоря уже о датчиках и исполнительных контроллерах, которые могут быть вообще где угодно.
Теперь появился агент, отвечающий за ключи. Он слушал события о появлении новых ключей и разрешал или запрещал срабатывания.
пример агента-ключника
#!/usr/bin/perl -w $|=1; use Net::MQTT::Simple; use DBI; use JSON; use Data::Dumper; $SIG{CHLD} = "IGNORE"; my $agent_name = 'keyholder'; ######################################## my $mqtt = Net::MQTT::Simple->new("192.168.1.*"); my $db_user='************'; my $db_name='agents'; my $db_pass='************'; my $db_host='192.168.1.*'; my $dbh=DBI->connect("dbi:Pg:database=$db_name;host=$db_host", $db_user, $db_pass ) || die "Cant connect database\n"; ######################################## # device database my $lock1 = '42668688'; my $lock2 = '6356544'; #------------------------------------------ # send MQTT message sub pub { my ($topic, $message, $retain) = @_; my $pid = fork(); if(defined $pid && $pid == 0){ if($retain){ $mqtt->retain($topic => $message); }else{ $mqtt->publish($topic => $message); } sleep(2); exit(0); } } sub check_key { my ($lock, $key) = @_; # search my $sth = $dbh->prepare("select * from door_keys where key_id = ? and lock_id = ?"); $sth->execute($key, $lock); if(my $row = $sth->fetchrow_hashref){ if($row->{allowed} == 1){ pub("mesh/to/$row->{lock_id}","open"); } $dbh->do("update door_keys set last_dtm=? where key_id=? and lock_id=?", undef, time, $row->{key_id}, $row->{lock_id}); } else{ $dbh->do("insert into door_keys (dtmcreate, key_id, lock_id) values (?,?,?)", undef, time, $key, $lock); } $sth->finish; } #------------------------------------------ # processing print "Started\n"; $mqtt->run( "agent/$agent_name" => sub { my ($topic, $message) = @_; if($message eq 'restart'){ print "Restaring\n"; exit; } }, "mesh/from/$lock1" => sub { my ($topic, $message) = @_; if($message =~ /^{.*}$/){ my $data = from_json($message); .... } elsif($message =~ /alarm ([\dABCDEF:]{23})/){ my $key = $1; check_key($lock1, $key); } }, "mesh/from/$lock2" => sub { my ($topic, $message) = @_; if($message =~ /^{.*}$/){ my $data = from_json($message); .... } elsif($message =~ /alarm ([\dABCDEF:]{23})/){ my $key = $1; check_key($lock2, $key); } }, ); exit;
Так оно проработало много лет, пока наконец электромагнитное реле, включавшее замок, не начало глючить.
Видимо подгорели контакты - оно щелкало, но срабатывало через раз, а в последнее время и вообще приходилось по несколько раз подносить ключ, чтобы сработало.
Можно было бы просто заменить реле на новое - но раз уж переделывать, то почему бы не поставить туда MOSFET?
А заодно убрать наконец "паука" и сделать по-нормальному, на печатной плате.
Вот это и было то самое последнее обновление: вместо "паука" - компактная печатная плата, вместо механического реле - транзистор, и ключи в базе.
Конкретная схема ничего особенного из себя не представляет, кто хоть раз делал "моргалку светодиодом" прекрасно сделает и такое.
Из особенностей можно отметить только то, что у ESP есть неприятная особенность - при перезапуске на всех выводак кратковременно появляется логическая 1, т.е. транзистор ��спеет открыться и откроет замок.
Но есть и другая особенность: для старта в нормальном режиме нужно прижимать GPIO15 к земле, напрямую или через резистор, а вот после старта можно использовать как обычный пин ввода-вывода.
И вот его-то и можно использовать для управления транзистором: в начале на нем по-любому будет 0, и ничего не будет открываться без команды.
Всё остальное остается без изменений.
И вот плата изготовлена (часик не торопясь, не считая разводки платы в программе), новый чип ESP припаян и прошит, ключи попали в базу и для них проставлены доступы.
Всё работает.
В перспективе - будут еще подобные замки на некоторых других дверях, там даже переделывать ничего не нужно - разве что считыватель "поизящнее" и коробку с контроллером в стену спрятать...
