29 мая 2015 в 16:36

Мой интернет вещей: Гостевой замок из песочницы

Так сложились обстоятельства, что имеется у меня однокомнатная квартира, в которой я не живу, а сдавать ее «обычным способом» мне не интересно. Попробовал я ее сдать через сервис Airbnb, понравилось. И не то, что бы это выгоднее, но точно интереснее, процесс захватывает. Но я не об этом…

Было у меня пару раз ситуация, когда я не мог лично встретить гостя и вручить ему ключ. Обычно в таких ситуациях приходится придумывать различные способы, от закладывания ключа под коврик до передачи через консьержку. Мне же не хочется посвящать в свои дела посторонних и как-то не комильфо прятать ключ под ковриком.

Хотелось что-то высокотехнологичное. Мыслил я так: смартфоны сегодня есть у многих, а если рассматривать моих потенциальных гостей, то почти у всех. Неужели ничего не придумано для iPhone и Android, чтобы со смартфона открывать дверной замок? Оказывается, очень даже придумано. Нашел два интересных решения.

1. Lockitron Bolt
Подробно о гаджете
Физически — это накладка-сервопривод на обычный дверной замок, управляемая по Bluetooth. Дополнительно имеется модуль Bridge, с одной стороны подключаемый к Интернет, а другой стороны — управляющийся по беспроводному каналу сервоприводом. Управляется это все через приложение для смартфонов и через веб-морду. Можно отправлять электронные ключи друзьям, что бы они сами вошли, можно открыть для ни них дверь через Интернет.

Цена вопроса $99 за сервопривод + $79 за Bridge + доставка.

В целом гаджет интересный, но есть пара проблем. Во-первых, он еще не готов. Принимаются предварительные заказы на бета-версию. Во-вторых, сервопривод рассчитан на отпирание и запирание замка в пол-оборота, именно такие замки популярны на его родине в США. У меня же в квартире цилиндровый замок европейского типа, для открытия/закрытия нужно сделать минимум 1 полный оборот цилиндра. Можно, конечно, врезать в дверь дополнительный замок, но см. «во-первых».

2. Kevo Smart Lock
Подробно о гаджете
Это самодостаточное устройство, замок со встроенным сервоприводом, Bluetooth-интерфейсом и сенсорным датчиком. Выглядит более привлекательно. В качестве «ключа» используется приложение для смартфона, специальный брелок или обычный ключ. Принцип работы похож на штатную сигнализацию некоторых современных автомобилей. Если в зоне действия беспроводной связи есть нужный телефон или брелок, то достаточно только прикоснуться к замку для открытия. Очень эффектно.

Цена вопроса $202 + доставка.

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

Выяснилось, что тяжелые металлические двери толщиной 60 мм в человеческих жилищах в США не очень популярны, вследствие чего и умные замки для таких дверей тоже не очень популярны. «Американский» тип замка устанавливается в дверь толщиной до 55 мм, как правило, деревянную.

Это все было обоснование проекта. Теперь собственно сама разработка.

Немного о себе. Работаю в сфере ИТ, но с контроллерами моя работа никак не связана. Имею базовое образование по робототехнике, по специальности не работал, но не все из ВУЗовского обучения успел забыть. Архитектура «изделия» у меня в голове нарисовалась сразу, без моего вербального участия.

Выглядела она так:

Командный сервер <=> Контроллер <=> Исполнительный механизм

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

Параллельно я думал над выбором исполнительного устройства. Во всех найденных в Сети примерах цилиндр замка жестко соединялся с сервоприводом, и меня это категорически не устраивало, т.к. нужно было, чтобы замок можно было открыть снаружи ключом, а жесткое соединение такую возможность исключало. Ну и опять же для открытия/закрытия моего замка нужен минимум один полный оборот цилиндра, а сервоприводы обычно поворачиваются только на 180°. Были мысли сделать сложный сервопривод, состоящий из приводного мотора, сцепления и датчика угла, но я ее отверг в следствии отсутствия металлообрабатывающего оборудования, которое непременно потребовалось бы для реализации этой задачи.

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

Наткнулся на электромеханический замок CISA стоимостью в 35 т.р. Можно было поменять свой замок (того же производителя) на него, благо размеры стандартные, но цена…

Еще один параллельный процесс — выработка функциональных требований к системе. Тут и далее будем называть систему «Гостевой замок». Итак:
  1. Гостевой замок не является основным замком, обеспечивающим безопасность квартиры;
  2. Гостевой замок используется только для того, что бы гости могли войти первый раз в квартиру в отсутствии хозяина и могли в отсутствии хозяина последний раз из нее выйти, и при этом не требовалось передавать ключ через почтовый ящик или каким-либо иным небезопасным способом;
  3. Остальное время квартира запирается на обычные замки с обычными металлическими ключами, соответственно Гостевой замок может быть в активном и неактивном состоянии;
  4. Гостевой замок должен быть безопасен с точки зрения пожарной безопасности, не препятствовать эвакуации при отключенном электропитании;
  5. Хозяин должен иметь возможность дистанционно через Интернет открыть Гостевой замок, перевести его в активное или неактивное состояние;
  6. Гость может воспользоваться «гостевым ключом», полученным от хозяина, для открытия замка;
  7. «Гостевой ключ» срабатывает только в непосредственной близости от замка;
  8. «Гостевой ключ» ограничен по времени действия.

Предстояло сделать мучительный выбор контроллера между Arduino Ethernet и Arduino Yun. Казалось бы, Yun — самое то, что доктор прописал. Поднимаем на линуксовой части веб-сервер, реализуем на нем всю логику высокого порядка и в нужный момент отдаем команды на микроконтроллерную часть. Остается только купить у провайдера настоящий IP-адрес и все заработает. Но что-то меня останавливало от такого выбора. Сначала это были тревожные сомнения о том, что в квартиру нужно провести настоящий IP, а потом я понял, что на самом деле меня останавливает масштабируемость. Один замок на Yun заработает на «раз-два», а два замка? А если у двух замков разные хозяева и гости? И т.д.

Таким образом, окончательный выбор пал на Arduino Ethernet.

Тем временем решился вопрос с исполнительным механизмом. В качестве такового был выбран электроригельный замок YLI для систем контроля и управления доступом, ценой чуть более 3 т.р. Такие стоят в офисах, на складах и т.п. помещениях, как правило в комплекте с кучей другого СКУД-оборудования.

Питание 12В, максимальный пусковой ток соленоида 900 мА. Особая фишка для пользователей металлических дверей: его можно встроить в дверную коробку, ширина 37 мм (коробки обычно 40-50 мм).

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

От замка идет 5 проводов:
Красный: +12В
Черный: Земля
Оранжевый: Открытие. На нем обычно +4,8 В. Для того, что бы открыть замок нужно замкнуть провод на землю.
Зеленый и Белый: Датчик закрытия двери. Когда дверь открыта, провода разомкнуты, когда закрыта — замкнуты.

Итак, соберем первый прототип


Управлять 12 В цепью питания замка будем через полевой транзистор. Замыкать на землю провод открывания будем через N-P-N транзистор. Для красоты поставим двухцветный светодиод, что бы светил зеленым, когда замок не активен и красным, когда активен. Для придания драматизма всей сцене поставим пьезопищалку. Для ручного открытия изнутри, как на дверях с домофоном, поставим тактовую кнопку.

Скетч принимает команды по UART, распознает их и умеет активировать, деактивировать и открывать замок. Кроме того отвечает на запросы о текущем статусе замка (активен или нет) и состоянии двери (открыта или закрыта)

image

Скетч
#include <EEPROM.h>

// Цифровые и аналоговые входы/выходы
#define BEEPER  5  // Пищалка
#define BUTTON  2  // Кнопка
#define LED_RED 6  // Светодиод активированного замка
#define LED_GREEN  3 // Светодиод деактивированного замка
#define LOCK_POWER  7 // Затвор полевого транзистора, управляющего цепью питания замка
#define LOCK_OPEN  9 // Вход транзисторного ключа, открывающего замок
#define NO_PIN  8 // Датчик открытой двери

// Константы событий для основного цикла loop()
#define EVENT_NONE  0  // Ничего не происходит
#define EVENT_BUTTON  1 // Нажата кнопка
#define EVENT_ACTIVATE  2 // Получена команда активации замка
#define EVENT_DEACTIVATE  3 // Получена команда деактивации замка
#define EVENT_OPEN  4 // Получена команда открыть замок
#define EVENT_SERIAL  99 // Получена строка через UART

char deviceID[12];

int eventButton = EVENT_NONE;
int lockActive = LOW;

String inputString = "";

void pressButton(){ // Обработчик прерывания нажатия кнопки
  eventButton = EVENT_BUTTON;
}

void setup(){
  pinMode(BEEPER, OUTPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(LOCK_POWER, OUTPUT);
  pinMode(LOCK_OPEN, OUTPUT);
  pinMode(BUTTON, INPUT);
  pinMode(NO_PIN, INPUT);
  Serial.begin(9600);


  inputString.reserve(30);
  attachInterrupt(0, pressButton, RISING);
 
  eventButton = EVENT_NONE;
  lockActive = EEPROM.read(0); // Считываем настройку активации замка "по умолчанию"
 
  digitalWrite(LOCK_POWER, lockActive);
  digitalWrite(LED_RED, lockActive);
  digitalWrite(LED_GREEN, !lockActive);
}

int commandProcessor(String incomingString){
  incomingString.trim();
  incomingString.toUpperCase();
  if (incomingString == "OPEN") {
    return EVENT_OPEN;
  }
  else if (incomingString == "ACTIVATE") {
    return EVENT_ACTIVATE;
  }
  else if (incomingString == "DEACTIVATE") {
    return EVENT_DEACTIVATE;
  }
  else {
    return EVENT_NONE;
  }
}

void loop(){

  // Предварительный обработчик событий от интерфейсов

  if (eventButton == EVENT_SERIAL){
    // сначала обработка только команд от Serial
    inputString.trim();
    inputString.toUpperCase();
    if (inputString == "STATUS") { //Запрос статуса замка
      if (lockActive == HIGH) {
        Serial.println("ACTIVE");
      }
      else {
        Serial.println("NOTACTIVE");
      }
    }
    else if (inputString == "DOOR") { // Запрос состояния двери по датчику
      if (digitalRead(NO_PIN) == HIGH) {
        Serial.println("CLOSE");
      }
      else {
        Serial.println("OPEN");
      }
    }
    else if (inputString == "NORMAL OPEN") { // Настройка начального состояния замка
      if (EEPROM.read(0) != LOW) {
        EEPROM.write(0, LOW);
        delay(10);
      }
    }
    else if (inputString == "NORMAL CLOSE") {
      if (EEPROM.read(0) != HIGH) {
        EEPROM.write(0, HIGH);
        delay(10);
      }
    }

    // Затем общие команды
    eventButton = commandProcessor(inputString);

    inputString = "";
  }

  // Основной обработчик событий

  if (eventButton == EVENT_BUTTON) {
    eventButton = EVENT_OPEN; // что делать при нажатой кнопке решаем здесь и идем дальше
  }
  else if (eventButton == EVENT_ACTIVATE) {
    lockActive = HIGH;
    digitalWrite(LOCK_POWER, HIGH);
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GREEN, LOW);
    tone(BEEPER, 700, 50);
  }
  else if (eventButton == EVENT_DEACTIVATE) {
    lockActive = LOW;
    digitalWrite(LOCK_POWER, LOW);
    digitalWrite(LED_RED, LOW);
    digitalWrite(LED_GREEN, HIGH);
    tone(BEEPER, 700, 50);
  }  
  else if (eventButton == EVENT_OPEN) {
    digitalWrite(LOCK_OPEN, HIGH);
    if (lockActive == HIGH) {
      tone(BEEPER, 750, 50);
    }
    delay(10);
    digitalWrite(LOCK_OPEN, LOW);
    eventButton = EVENT_NONE;
  }

  eventButton = EVENT_NONE;
}

void serialEvent() {
  while (Serial.available()) {
    char inChar = (char)Serial.read();
    inputString += inChar;
    if (inChar == '\n') {
      eventButton = EVENT_SERIAL;
    }
  }
}


Выводы по первой итерации проекта.
  1. Arduino — это Весчь!
  2. Инженерные навыки не пропьешь, оказывается, я много чего помню;
  3. Ethernet Shield резервирует много выходов Arduino Uno, если будет не хватать на светодиоды, придется экономить. Например, затвор полевого транзистора и красный светодиод можно подключить к одному пину, кнопку открытия не обязательно заводить в контроллер, можно сразу на замок или транзисторный ключ.


Добавим немного распределенности


В результатах гугления по запросу «управление arduino интернет» в основном рассказывается, как сделать простой веб-сервер для индикации показаний датчиков, или как на Yun построить селфи-будку для кота.

На Хабре был интересный обзор облачного сервиса Ninja Blocks. Похоже, что когда я читал этот обзор, сервис работал как-то не так.

Тот же автор ivizil совсем недавно опубликовал способ управления через HTTP-запросы. Я этот способ опробовал примерно за месяц до этой публикации и отверг его по соображениям эстетичности. Бомбить хостинг каждые 5-10 секунд одним и тем же запросом в ожидании одной единственной команды в сутки как-то не эстетично.

Идеальный вариант мне виделся в использовании XMPP. Клиент на Arduino подключается к серверу и получает от него команды: «включись», «выключись», «откройся» или запросы «состояние», «дверь». Гость тоже подключается как XMPP клиент, сервер разграничивает доступ и контролирует кто, какую и куда команду может отправить.

Увы, никаких готовых библиотек для реализации XMPP клиента на Arduino не нашлось. Будем довольствоваться старым добрым Telnet.

Второй прототип


В целях экономии цифровых выходов попробовал посадить затвор полевого транзистора и светодиод на один выход. Замку резко стало не хватать напряжения для вытягивания ригеля. Замеры показали, что на замке вместо необходимых 12В чуть менее 11В. Проблема в том, что когда на затворе 5В, между истоком и землей 11,8В, а когда 4,8В — 10,9В. Придется перед затвором поставить биполярный транзистор в цепь 12В, ну и учесть нюанс, что этот транзистор будет немного инвертировать сигнал.

Для второго прототипа нам потребуется Telnet-сервер. Написать его можно много на чем, я решил это сделать на Python. Признаюсь, мое знакомство с Python весьма шапочное. Я читал, что на нем очень быстро и удобно работать, что он кросплатформенен и даже смог с первой попытки написать «Hello World!».

Начал гуглить примеры работы с Telnet на Python и довольно быстро наткнулся на библиотеку Twisted. За основу взял пример простого чат-сервера и доработал его до «командного сервера».

Сценарий работы выглядит так:

При включении контроллер пытается подключиться к серверу. При неудачной попытке следующая повторяется через некий интервал времени. допустим 10 секунд.

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

На сервере мы помечаем клиента, подключившегося таким способом как «замок».

«Ключи» подключаются похожим способом. После соединения клиент отправляет на сервер запрос на то, чтобы стать «ключом». Та же запросная авторизация: сервер отвечает случайной последовательностью символов, к ней надо добавить секретное слово, которое ключ должен знать, вычислить MD5 и отправить обратно. Если результат, отправленный клиентом совпадет с результатом, рассчитанным сервером, клиент определяется как «ключ» и ему разрешается отправлять команды «замкам».

В обработчике событий протокола парсим полученные от клиентов строки и пересылаем туда-сюда команды.

Код сервера на Python
from twisted.protocols import basic
from twisted.internet import reactor
from twisted.internet.protocol import ServerFactory 
from twisted.protocols.basic import LineOnlyReceiver
from twisted.application import service, internet
import hashlib 
import threading
import MySQLdb
from MySQLdb import Error
import random


class ChatProtocol(LineOnlyReceiver): 

    name = ""
    isKey = False
    authKey = ""
    
    def getSecret(self,isKey):
        if not self.factory.conn:
            self.factory.mySQLdbConnect()
        if isKey:
            reqTable = "lockkeys"
        else:
            reqTable = "locks"
        try:
            cursor = self.factory.conn.cursor()
            cursor.execute("SELECT `secret` FROM `" + reqTable + "` WHERE id='" + self.getName() + "'")
            rows = cursor.fetchall()
                        
            if cursor.rowcount > 0:
                cursor.close()
                return rows[0][0].strip() 
            else:
                cursor.close()
                return None
            
        except Error as e:
            print(e)
            return None

        finally:
            cursor.close()
            
    def validKey(self,lockID):
        if not self.isKey:
            return False
        if self.transport.getPeer().host == "127.0.0.1":
            return True
        if not self.factory.conn:
            self.factory.mySQLdbConnect()
        try:
            cursor = self.factory.conn.cursor()
            cursor.execute("SELECT `rights` FROM `hosts` WHERE (`lock`='" + lockID + "') AND (`lockkey`='"+ self.getName() + "')")
            rows = cursor.fetchall()
            
            if cursor.rowcount > 0:
                cursor.close()
                return rows[0][0]
            else:
                cursor.close()
                return False
        except Error as e:
            print(e)
            return False

        finally:
            cursor.close()            

    def getName(self): 
        if self.name!="": 
            return self.name 
        return self.transport.getPeer().host 

    def connectionMade(self): 
        print "New connection from "+self.getName()
        if self.transport.getPeer().host == "127.0.0.1":
            self.isKey = True 
        d = {self.getName() : self}
        self.factory.clients.update(d)

    def connectionLost(self, reason): 
        print "Lost connection from "+self.getName() 
        self.factory.clients.pop(self.getName()) 
        self.factory.sendMessageToAllclients(self.getName()+" has disconnected.") 
        

    def lineReceived(self, line): 
        #print self.getName()+" said "+line
        #if line[:3] == "/OK"
        if line[:5]=="/KEY:":
            str1 = '123456789'
            str2 = 'qwertyuiopasdfghjklzxcvbnm'
            str3 = str2.upper()
            str4 = str1+str2+str3
            ls = list(str4)
            random.shuffle(ls)
            self.authKey = ''.join([random.choice(ls) for x in range(12)])
            self.sendLine("ANSW:"+self.authKey)
            oldName = self.getName() 
            self.name = line[5:].strip() 
            self.factory.clients.pop(oldName)
            self.factory.clients.update({self.getName() : self})
            self.isKey = False
            print oldName+" has requested to be Key ID:"+self.getName()
        elif line=="/EXIT": 
            self.transport.loseConnection()
        elif line[:6]=="/ANSW:":
            secret = self.getSecret(True)
            if secret:
                m = hashlib.md5()
                m.update(self.authKey + secret)
                if m.hexdigest() == line[6:].strip():
                    self.isKey = True
                    print self.getName() + " was authorisated as Key"
                else:
                    self.sendLine("Authorisation fail")
                    self.transport.loseConnection()             
            else:
                self.sendLine("Authorisation fail")
                self.transport.loseConnection()


                
        if self.isKey: #Only from keys
            
            if line[:6]=="/OPEN:":
                if self.validKey(line[6:]):
                    adresat = self.factory.clients.get(line[6:],None)
                    if adresat:
                        adresat.sendLine("OPEN:" + self.getName() +  "\r\n")
                    else:
                        self.sendLine("DeviceID="+ line[6:] + " is not online\r\n")
            elif line[:10]=="/ACTIVATE:":
                if self.validKey(line[10:]):
                    adresat = self.factory.clients.get(line[10:],None)
                    if adresat:
                        adresat.sendLine("ACTIVATE:" + self.getName() +  "\r\n")
                    else:
                        self.sendLine("DeviceID="+ line[10:] + " is not online\r\n")
            elif line[:12]=="/DEACTIVATE:":
                if self.validKey(line[12:]):
                    adresat = self.factory.clients.get(line[12:],None)
                    if adresat:
                        adresat.sendLine("DEACTIVATE:" + self.getName() +  "\r\n")
                    else:
                        self.sendLine("DeviceID="+ line[12:] + " is not online\r\n")           
        else: # Only from locks
            if line[:4]=="/ID:": 
                oldName = self.getName() 
                self.name = line[4:].strip() 
                self.factory.clients.pop(oldName)
                self.factory.clients.update({self.getName() : self})
                print oldName+" changed name to "+self.getName() 
            elif line[:4]=="/RE:":
                secret = self.getSecret(self.isKey)
                if secret == None:
                    self.sendLine("Your Device ID is not register in Command Server")
                    self.transport.loseConnection()
                else:
                    requestCode = line[4:]
                    m = hashlib.md5()
                    m.update(requestCode + secret)
                    self.sendLine("AUTH:" + m.hexdigest() + "\r\n")
            elif line[:4]=="/OK:":
                adresat = self.factory.clients.get(line[4:],None)
                if adresat:
                    adresat.sendLine("OK:" + self.getName())
            else: 
                self.factory.sendMessageToAllclients(self.getName()+" says "+line)
    
    def sendLine(self, line): 
        self.transport.write(line+"\r\n") 
        
        

class ChatProtocolFactory(ServerFactory): 

    protocol = ChatProtocol 
    clients = {} 
    
    def mySQLdbConnect(self):
        try:
            self.conn = MySQLdb.connect(host='127.0.0.1',user='guestlock',passwd='passw',db='guestlock')
        except Error as e:
            print(e)
            return False
        else:
            return True
            
    def __init__(self):
        print "Starting server..." 
        self.clients = {}
        if self.mySQLdbConnect():
            print "Server ready!"
        else:
            print "Data base error. Server doesn't work correctly."
        

    def sendMessageToAllclients(self, mesg): 
        for client in self.clients.values():
            client.sendLine(mesg) 


factory = ChatProtocolFactory()
application = service.Application("CommandServer")
internet.TCPServer(12345, factory).setServiceParent(application)


Сервер следует запустить как демон с помощью штатной утилиты демонизации twistd
$twistd -y CommandServer.py


Скетч
#include <EEPROM.h>
#include <SPI.h>
#include <Ethernet.h>
#include <MD5.h>

// Цифровые и аналоговые входы/выходы
#define BEEPER  5  // Пищалка
#define BUTTON  2  // Кнопка
#define LOCK_POWER 7  // Замок активирован 
#define LED_DEACTIVE  6 // Замок деактивирован  
#define LOCK_OPEN  9  // Открытие замка
#define LED_ETHERNET  8 // Индикатор подключения к серверу
#define NO_PIN  A0  //Датчик NO замка АНАЛОГОВЫЙ


// Константы событий для основного цикла loop()
#define EVENT_NONE  0
#define EVENT_ETHERNET 98
#define EVENT_SERIAL  99


#define RECONNECT_TIME  5000 // Интервал попыток подключения к серверу, мс


char deviceID[12];
byte mac[] = { 
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
EthernetClient client;
IPAddress server(192,168,1,7);
long port = 12345;

unsigned long lastConnectionTime = 0;       
unsigned long disconnectTime = 0;
boolean lastConnected = false;                

volatile int eventButton = EVENT_NONE;  // Индикатор события для основного цикла loop()
int lockActive = LOW;

String inputString = "";        
String ethernetString = "";

long requestCode;

void pressButton(){ // Обработчик прерывания нажатия кнопки
  lockOpen();
}

void majorBeep() {
  tone(BEEPER, 750, 50);
}

void minorBeep() {
  tone(BEEPER, 300, 100);
}


void lockActivate(boolean lockState){
  digitalWrite(LOCK_POWER, lockState);
  digitalWrite(LED_DEACTIVE, !lockState);
  lockActive = lockState;
  majorBeep();
}

void lockOpen() {
  digitalWrite(LOCK_OPEN, HIGH);
  delay(50);
  digitalWrite(LOCK_OPEN, LOW);
  majorBeep();
}

void lockSetDefaultState(int state){
  if (EEPROM.read(0) != state) { 
    EEPROM.write(0, state); 
    delay(10);
  }
  majorBeep();
}

boolean serverAuthRequest() {
  if (!client.connected()){
    return false;
  }
  client.print("/ID:");
  client.println(deviceID);
  delay(1000);
  char rCode[6];
  ultoa(random(99999),rCode,16);

  unsigned char* hash=MD5::make_hash(rCode);
  char *md5str = MD5::make_digest(hash, 16);

  client.print("/RE:");
  client.println(md5str);

  String answerHash = md5str;
  answerHash.trim();
  for (int i=0; EEPROM.read(50+i) != 0; i++){
    answerHash += char(EEPROM.read(50+i));
  } 

  char *aCode = (char*)malloc(answerHash.length()+1);
  ;
  answerHash.toCharArray(aCode,answerHash.length()+1);

  hash=MD5::make_hash(aCode);
  md5str = MD5::make_digest(hash, 16);

  answerHash = md5str;

  free(md5str);
  free(aCode);

  unsigned long mm = millis();
  ethernetString = "";
  while (millis()-mm < RECONNECT_TIME) {
    if (client.available()) {
      char inChar = client.read();
      ethernetString += inChar;
      if (inChar == '\n') {
        ethernetString.trim();
        if (ethernetString.startsWith("AUTH:")) {

          ethernetString = ethernetString.substring(5);
          break;
        } 
        else {
          ethernetString = "";
        }
      } 
    }
  }
  return (ethernetString == answerHash);
}


boolean serverConnect() {
  if (client.connect(server, port)) {
    lastConnectionTime = millis();
    if (serverAuthRequest()) {
      Serial.println("Server autenfication sucsesseful");
      majorBeep();
      digitalWrite(LED_ETHERNET, HIGH);
      ethernetString = "";
      return true;
    } 
    else{
      Serial.println("Server autenfication fail");
      minorBeep();
      digitalWrite(LED_ETHERNET, LOW);
      ethernetString = "";
      client.stop();
      return false;
    }
  }   
  else {
    Serial.println("Connection fail");
    client.stop();
    digitalWrite(LED_ETHERNET, LOW);
    disconnectTime = millis();
    ethernetString = "";
    return false;
  }
}


void setup(){
  pinMode(BEEPER, OUTPUT);
  pinMode(LOCK_OPEN, OUTPUT);
  pinMode(BUTTON, INPUT);
  pinMode(LOCK_POWER, OUTPUT);
  pinMode(LED_DEACTIVE, OUTPUT);
  pinMode(LED_ETHERNET, OUTPUT);


  lockActivate(EEPROM.read(0));

  Serial.begin(9600);

  randomSeed(analogRead(A1));

  for (int i=1; i<12; i++) {
    deviceID[i-1] = EEPROM.read(i);
  }

  Serial.print("Device ID: "); 
  Serial.println(deviceID); 

  delay(1000);
  // start the Ethernet connection using a DNS server:
  Ethernet.begin(mac);

  Serial.print("My IP address: ");
  Serial.println(Ethernet.localIP());

  if (!serverConnect()) {
    disconnectTime = millis();
  }

  inputString.reserve(30);
  attachInterrupt(0, pressButton, RISING);
  eventButton = EVENT_NONE;
}

boolean commandProcessor(String incomingString, int commandSource){
  
  boolean result = true;
  String report = "OK";
  incomingString.trim();
  incomingString.toUpperCase();
  if (incomingString.startsWith("OPEN")) {
    lockOpen();
  } 
  else if (incomingString.startsWith("ACTIVATE")) {
    lockActivate(HIGH);
  } 
  else if (incomingString.startsWith("DEACTIVATE")) {
    lockActivate(LOW);
  } 
  else if (incomingString.startsWith("STATUS")) {
    if (lockActive == HIGH) {
      report = "ACTIVE";
    } 
    else {
      report = "NOTACTIVE";
    }    
  }
  else if (incomingString.startsWith("DOOR")) {
    if (analogRead(NO_PIN) > 1000) {
      report = "CLOSE";
    } 
    else {
      report = "OPEN";
    }
  }
  else {
    result = false;
  }

  if (result && (commandSource == EVENT_ETHERNET) && client.connected()) {
    if (incomingString.indexOf(":") > 0) {
      client.print("/" + report + incomingString.substring(incomingString.indexOf(":")));
    } 
    else {
      client.println("/" + report);
    }    
  }
  
  if (result && (commandSource == EVENT_SERIAL)) {
    Serial.println(report);
  }

}



void ethernetEvent() {
  if (client.available()) {
    char inChar = client.read();
    ethernetString += inChar;
    if (inChar == '\n') {
      eventButton = EVENT_ETHERNET;
    } 
  }
}


void loop(){

  if (eventButton == EVENT_ETHERNET) {
    commandProcessor(ethernetString, eventButton);
    ethernetString = "";
    eventButton = EVENT_NONE;
  }

  if (eventButton == EVENT_SERIAL) {
    commandProcessor(inputString, eventButton);
    inputString = "";
    eventButton = EVENT_NONE;
  }

  if (!client.connected() && lastConnected) {
    Serial.println("Disconnecting.");
    minorBeep();
    digitalWrite(LED_ETHERNET, LOW);
    disconnectTime = millis();
    client.stop();
  }

  lastConnected = client.connected();

  if ((!lastConnected) && (millis() > disconnectTime+RECONNECT_TIME)) {
    disconnectTime = millis();
    serverConnect();
  }

  ethernetEvent();
}

void serialEvent() {
  while (Serial.available()) {
    char inChar = (char)Serial.read();
    inputString += inChar;
    if (inChar == '\n') {
      eventButton = EVENT_SERIAL;
    } 
  }
}


ProtoShield


В аппаратной части контроллер видится мне логически завершенным, если что и захочу добавить, так это пару светодиодов для индикации каких-либо важных или не очень событий или состояний, а значит, самое время собрать его по-человечески. Сейчас это все выглядит как художественная инсталляция под названием «Какой провод резать?!!».

image

Закупаем Arduino ProtoShield за 240 руб. Паяльник, флюс и припой есть.

image

Шилд замка

Выводы по второй итерации:
  1. Python штука мощная, но непривычная;
  2. Паять я не разучился;
  3. Сервер будет работать на Rapsberry Pi;
  4. Надо заказать у провайдера реальный IP-адрес;
  5. Можно устанавливать «изделие» на «объект» и принимать гостей.

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

Продолжение следует...
Дмитрий @kuld
карма
8,0
рейтинг 0,0
Разработка и коммерциализация
Самое читаемое

Комментарии (58)

  • 0
    Электромеханический замок и карты Proximity? Сетевой контроллер, подключенный к компу по RS-485. Как-то так это делается, если совсем без велосипедов.
    • +1
      А как карты Proximity решат задачу
      Было у меня пару раз ситуация, когда я не мог лично встретить гостя и вручить ему ключ
      ?
      • +1
        Так тут пока эта задача тоже не решена. По этой теме — мой второй комментарий.
        Карты хороши просто как ключ для гостя. Например, беспроблемным удалением из системы, в отличие от механического ключа.
  • +1
    Можно поставить антивандальную цифровую кнопочную панель, а гостю отправлять SMS с одноразовым кодом входа со временем жизни, скажем, 1 минута.
    • 0
      И смысл? Только дороже получится и сложнее.
      • 0
        Никаких смартфонов и приложений для них. Никакого блютуса. Полностью решается проблема, когда невозможна физическая передача ключа.
        • 0
          Ну камеры хранения на вокзале или любой кодовый механический замок точно так же решают ту же проблему. Тут же про то что Arduino совместимые устройства (такие как Olimex ESP8266-EVB) имеют не высокую цену и низкий порог вхождения для составления программы и открытый исходный код для программ и OSHW.
          • 0
            GSM модули, беспроблемно цепляемые к ардуине, стоят примерно от 25$. Если надо ультрабюджетно, то в интернет вообще ничего не заводится, делается свой аналог простейшей GSM-сигнализации, все управление SMS-командами.

            Кодовый замок ничего не решает. Идея в одноразовости (неподбираемости) кода. Т.е., подходит человек к двери, отправляет на номер шлюза пустое сообщение, если его телефон есть в списке доступа — получает код для панели.

            Можно вообще открывать дверь по SMS, но у автора темы есть требование (не знаю точно, зачем), чтобы гость мог открыть дверь только непосредственно находясь перед ней. Пункты 6-8 ТЗ.
            • 0
              Итого имеем:
              Arduino Nano — 3$;
              Плата с SIM900 — 30$;
              Металлическая клавиатура — 20$;
              Нормальный ЭМ-замок, примерно 40$;
              Резервируемый источник питания (от ОПС) — пусть еще 30$.
              Почти все. 123$ за железо, осталось собрать вместе и запрограммировать.
              • 0
                А если надо в интернет с вебмордами и прочими ништяками, то прилепить к этому делу еще сбоку малинку. Или использовать только ее, вместо ардуины. И тогда уж USB GSM модем.
                • +2
                  Написал ниже. Малинки не надо. ESP8266 достаточно для веб морды и подключения к WiFi через Веб, API. И цены у меня получаются 25 за всё вместе с замком + 15-20 за резевный источник питания с зарядкой.
                  • 0
                    Я не верю в замки дешевле 35-40 баксов. Были прецеденты, поэтому минимальное, что стараюсь использовать на практике — «Полис» или Yus. Чаще таки CISA. Идея с вайфаем ок, но я все равно больше симпатизирую GSM.

                    С ESP8266 пока не ковырялся, но, чувствую, придется, что-то их уже в каждую бочку затычкой советуют.
                    • 0
                      По поводу ESP8266 советую эту последовательность действий, как самую оптимизированую в плане Android приложения и API. А в плане максимума функций в одном ESP8266 эту. Инструкции для второй тут.
            • +1
              Дык ставим ESP8266 за 5 долларов + блок питания 3 доллара+ стабилизатор 5 долларов + зарядник аккумуляторов + аккумуляторы 10 долларов. Или ESP8266-EVB за 15 евро вместе с питанием. Оно же само по себе роутер. Подключаем к любой эклектрической защёлке за 5 долларов или за 30 (которая с батарейками и считывателем RFID).

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

              Человек подходит к двери. Не нужно не какие приложения. Просто подключается к сети на основе ESP8266 и в браузере мобильного телефона через jQuery вводит пароль (можно и WiFi через пароль сделать) и открывает дверь.

              Владелец квартиры (так как модуль может одновременно быть подключён к квартирному WiFi) через DDNS и встроенное API у ESP8266 может менять права доступа пользователей к открыванию замка.

              В дополнение можно поставить систему охраны, которая будет просто блокировать дверь с помощью решения от ABUS. На случай подозрительной активности около двери или просто когда хозяин точно знает что клиенты не используют помещение.
              • 0
                Насчет цен чуть-чуть поправлю. ESP-12 стоит 2.5$, блока питания достаточно простейшего за 0.4$, к нему нужен регулятор напряжения на 3.3В за 0.10$ (если паять) или за 0.4$, если модулем. А на сэкономленные деньги я бы предложил взять плату nodemcu для удобной отладки. :-)
                • 0
                  Цены это особенности Европейского рынка, они включают гарантию 2 года и минимальную поддержку в виде мануалов.
                  • 0
                    Не совсем понимаю на что именно гарантия — не на копеечный же микроконтроллер. Но слышал, что многие предпочитают покупать через Европу, чтобы не столкнуться с заведомым браком.

                    А в чем особенности ESP8266-EVB по сравнению с nodemcu dev board?
                    • 0
                      0. Наличие реле за примерно те же деньги.
                      1. Произведено в EU в Болгарии.
                      2. Отсутствие VAT (налогов) для зарегистрированных в EU компаний при ввозе в EU страну.
                      3. 2 Года гарантии а соответственно качество техпроцесса и комплектующих.
                      4. Знаю лично комьюнити OS и работников Olimex.
                      5. Доставка в Испанию за счёт компании в течении 2 денй.
                      6. Нет проблем с памятью (впрочем NodeMCU можно то же использовать без LUA)
                      7. Полноразмерная кнопка (можно поместить модуль в коробку с отверстием и получиться выключатель, который работает и от кноки и через WiFi).
                      8. Полностью открытый код и полностью открытый дизайн железа.
                      9. Наличие UEXT шины и к ней набора из 15+ сенсоров и экранов и других устройств.
                      10. Дравера и скетчи для всей этой периферии (надо только поменять номера GPIO в коде).

                      Но если смотреть в будущее, то вот это рекомендую.
                      • 0
                        Я как-то разочаровался в комбайнах. Мне не так уж трудно разместить модули на плате самому, а в комбайне редко используется хотя бы треть предустановленных. При этом резко вырастают габариты и цена. Встроенный линукс — это хорошо, но, опять же, в моем случае избыточно — у меня есть полноценный домашний сервер, принимающий решения. А внешние исполнительные модули должны быть готовы к работе за считанные секунды после включения.
                        • 0
                          Там главное 2 реле. Можно управлять открытием жалюзей. Либо надо использовать MOD-IO2 + MOD-WIFI-ESP8266. Но прошивки PIC и ESP для совместной работы этих двух модуей пока не допилены напильником.

                          Плюс указанного выше «комбайна» — возможность монтировать на рейку и DIN корпус (RT5350F-OLinuXino-DIN) а так же лучшая мощность сигнала (а значит дальность) и скорость. Что очень важно для реальных установок а не домашних поделок.

                          Указанные выше решения не выключаются.
                          • 0
                            Не совсем понял, что вы имеете в виду. У ESP минимум четыре беспроблемных GPIO вывода, которые могут рулить, соотв. четырьмя реле без каких-либо дополнительных усилий. Или еще большим количеством реле, посредством ShiftReg.
                            • 0
                              У MOD-WIFI-ESP8266-DEV 10 GPIO выведено и + UEXT разьём + 6 выводов от UEXT на пины. Но если вы продаёте устройства конечным клиентам, то не должно быть паянных вами частей, либо вы должны получить сертификат CE, который стоит более 5000 евро. Его надо получать для каждой новой модификации устройства.

                              Когда вы делает реальные системы для клиентов малые габариты устройства так же важны. Поэтому и потому что не китай — только ESP8266-EVB обладает маленькими габаритами и наличием реле и сделан в EU.

                              И не вспоминайте пожалуйста больше про LUA. На реальных системах есть требования к размеру памяти, которая нужна для множество функций. (От MQTT до DDNS и jQuery и API и пр.)
                              • 0
                                Уверяю, у меня и в мыслях не было заниматься промышленным производством, как и, судя по тексту, у автора топика. Как мне кажется, одна из серьезных причин развития движения DiY, это:

                                вы должны получить сертификат CE, который стоит более 5000 евро


                                Что же до LUA, то у этого выбора есть одно очень весомое преимущество, как и у любого интерпретатора — возможность легко загружать обновления кода. Я бы послушал об альтернативах с интерпретаторами, но совершенно не заинтересован ни в каких фреймворках, которые требуют подключения чипа к программатору для изменения одной константы в проекте.
                                • 0
                                  По моему вы опять сказали не подумав. В прошивках есть обновление по воздуху. Есть заливка через USB кабель JS кода и при этом нет ограничений памяти. На этом, позвольте, закончить дискуссию с вами.
                                  • 0
                                    Вы, как я понимаю, ссылаетесь на jQuery и NodeJS, где логика полностью вынесена на внешний сервер (или вовсе на стороне клиента), в результате чего собственные вычислительные возможности MCU не используются. Это подходит лишь для каких-нибудь реле и сенсоров, где полсекунды задержки не критичны и создает проблемы безопасности.

                                    Есть заливка через USB кабель JS кода


                                    Разобрать железную дверь, достать замок, разобрать замок, добраться до USB/UART микроконтроллера, залить обновление, собрать замок, собрать железную дверь. Похоже, мы говорим о принципиально разном уровне удобства разработки, так что соглашусь, дальнейшая дискуссия и впрямь теряет смысл.
  • +2
    Самый недорогой дверной электрический замок 5$

    Оптимальный замок цена/качество — 30$

    Специальная программа для системы на Arduino по управлению светом/замками/сенсорами c обменом состояниями и мешсетью. В качесвте управляющего центра используется Android устройство.

    Подробные инструкции как использовать не Arduino с Ethernet шильдом а ESP8266-EVB за 10 евро (включая WiFi и реле).

    Возможно подключить датчики движения, альтернативные WiFi приёмники и предающие модули, RFID.
    • 0
      замок за 5 баксов — это замочек для шкафчика для одежды или стола. Вряд ли такому можно доверить квартиру…
      • 0
        Это самый дешёвый замок на Алибабаба для бытовой техники (посудомоющих машин, холодильников), но его можно использовать как дополнительный замок.

        Возможно на Таобао дешевле.

        Вот он
  • 0
    Тут еще незахабренные просят обратить внимание на DanaLock.
    • +1
      Брендованная беспроводная защёлка ABUS
      image

      Secvest Key — защёлкивающийся беспроводным устройством замочный цилиндр (частота 433 МГц или 866 МГц).
      image
  • 0
    Вот совем уж матерый замочек
  • 0
    я пару лет назад интересовался — бронедвери с электронным замком с выводом на логику и подачу команд в принципе не так дорого стоят. да и сами замки тоже. а его хоть к ардуине хоть малинке, хоть к компу цепляй. причем с документацией. Но вендора, к сожалению не упомню, гуглить опять надо
  • 0
    Насчет резервирования выводов.

    Можно добавить микросхему 74HC595N, чтобы ценой трех пинов получить еще 8 (а при желании — 16, 24… и т.д.).

    Можно добавить в схему еще один Arduino Nano или Arduino Nano Pro, который будет связан с Uno через два пина .Uno + E.Shield будут заниматься коммуникацией, а Nano — всем остальным. Чуть дороже, чем первый вариант, зато возможностей больше.

    Можно вместо ethernet-shield поставить wifi-mcu ESP8266, который тоже отлично связывается с Arduino через два пина (и стоит в 10 раз меньше). Бонус — «непосредственная близость» физически обеспечивается зоной работы Wi-Fi.

    Можно обойтись двумя ESP, один из которых будет работать как точка доступа, а второй — общаться с сервером, чтобы таким образом не пускать в домашнюю Wi-Fi сеть всяких левых. GPIO на них хватит для управления хоть тремя замками.
    • 0
      Один ESP8266 прекрасно работает как точка доступа и в то же время цепляется к местной сети WiFi. Одного MOD-WIFI-ESP-DEV достаточно для управления 16 замками :-)
      • 0
        А как одновременно использовать station и AP режимы? Или вы предлагаете переключаться между режимами, когда нужно отправить запрос к серверу?
        • 0
          github.com/Ignat99/ESP8266_Relay_Board

          В этой прошивке используются сразу оба режима и каждый по отдельности.
          Можно обращаться к устройству как ESP_<часть мак адреса устройства> с IP 192.168.4.1
          И к местной WiFi сети через DHCP или через статический IP, который можно задать через веб интерфейс.
          • 0
            О! Спасибо за наводку. NodeMCU теперь тоже так умеет, оказывается:

            wifi.STATIONAP is a combination of wifi.STATION and wifi.SOFTAP. It allows you to create a local wifi connection AND connect to another wifi router.
  • 0
  • +1
    Пост не про поиск готового решения. Автор свое время инвестирует в написание статьи. Опытом делится.
    • +1
      А готовое решение может подсказать пару полезных идей.
    • 0
      Комментарии — резонанс на положения статьи. Резонанс вызвало спорное утверждение о проблемах с доступностью и ценой электрических замков, потому народ поспешил накидать вариантов. А польза статьи сомнению не подлежит, это лучший форм-фактор, когда свой опыт да на примерах.
    • 0
      del

      не в ту ветку
  • 0
    Не рассматривали для общения с сервером, так понимаю замки-контроллер в одной локалке, что-то на вида Pub-Sub, например Redis? Протокол простейший, можно хоть самому реализовать, либо попробовать адаптировать готовые библиотеки на C. При этом серверная часть должна стать проще.
  • 0
    А как же дверь подъезда? Говорить гостям, чтобы подождали когда кто-нибудь выйдет?)
    • 0
      Не на всех подъездах нужен ключ для входа. Возможно, у автора статьи нет такого.
  • 0
    Предлагаю в замке с модулем ESP8266 предусмотреть кнопку на PIN 0 (или использоват ESP8266-EVB совместно с VTX-214-003-105), которая может выполнять 2 функции: Открытие замка изнутри и прошивку устройства в случае нажатия кнопки до включения питания замка. 3 пина для прошивки можно вывести в ручку и прикрыть декоративной крышкой, как это сделано со скважиной для ключа в некоторых современных электронных замках.

    Но разработка механики IMHO выходит за рамки обсуждения.
  • 0
    Подумалось — а какой самый дешевый канал информации может дать телефон? Wi-Fi дешев, но у телефона есть еще и фонарик. Серия световых импульсов — код. Тогда на приемной стороне — всего лишь фотодиод. Связь в одну сторону, но для этой задачи достаточно. В кодовую посылку можно вставить и время жизни пароля. Вот только до сих пор не встречал приложения для подобного использования фонарика.
    Можно конечно и световым пятном на экране помигать.
    • 0
      Да художественным стуком можно дверь открывать. Выложу через пару часов пулл реквест. Потестил гироскоп. Работает. Фотодиод не катит для него отверстие нужно. Тогда уж ставить камеру. Благо есть IP камеры за 19$.
    • 0
      Bluetooth — он есть везде.
      Из перспективных (пока есть не везде, но скоро будут везде) — NFC.
      • 0
        Имел ввиду наиболее простой вариант приемной стороны. Отработать импульсы фотодиода сможет даже дешевая AtMega.
  • 0
    Идеальный сценарий — когда приезжающий получает одноразовый код ещё при бронировании/оплате, работающий в примерное время заезда туриста. В итоге мы отвязаны от всяких там смс-ок вайфаев которые могут сесть и т.п.
    • 0
      Ну так и вроде и есть. Вся информация в базе данных в момент бронирования. Только в данном случае база данных в голове владельца замка.

      Что бы батарейки не сели есть сообщение о их статусе. Для устройств есть ватчьдоги встроенные. Для WiFi можно сделать перезапуск на случай его исчезновения.

      Но думаю лучше всё таки поставить там камеру наблюдения и динамик на всякий экстренный случай который может случиться в любой момент. И ручное удалённое управление и двунаправленный звук, как в домофоне.
      • 0
        … а так же сигнализацию, шокер, лазер и кнопку самоуничтожения, запечатывающую дверь. А для мирных целей — простенький тетрис или змейку, чтобы ждущие у замка не скучали! :-)

        Все-таки камера — это уже другой уровень. Придется проводку вести и дырки сверлить. А значит исчезает смысл и в батарее.
        • 0
          Камеру можно встроить в глазок. Но пока, это дорого.

          Но если есть возможность разместить квадрат 30x30 mm внутри двери, то можно дешевле сделать. Такая камера модуль будет стоить 20$ + 15$ для WiFi модуля + 10$ для IP модуля.
          • 0
            Ну раз пошла такая пьянка, тогда можно на экран мобильника вывести просто QR-код и сунуть его в камеру
  • 0
    Можно использовать это
    http://habrahabr.ru/post/251083/

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