Буквально пару дней назад обновил свой старый замок на калитке, не планировал ничего об этом писать, но попалась статья 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 припаян и прошит, ключи попали в базу и для них проставлены доступы.
Всё работает.

В перспективе - будут еще подобные замки на некоторых других дверях, там даже переделывать ничего не нужно - разве что считыватель "поизящнее" и коробку с контроллером в стену спрятать...